OOP vs. services for organizing business logic

Is there a third way?

December 6, 2022 Ā· Felipe Vogel Ā·

Disclaimer: In this blog post I raise many questions and give few answers. At the bottom I list resources which Iā€™m exploring in search of an answer, so skip down if thatā€™s all you care about.

Business logic. Everyone has it, and no one seems to agree on where to put it in a Rails app. Some people stuff it all in Active Record models, others throw it out into service objects, and still others put it in POROs. (But then where do you put the POROs?)

In all these debates, thereā€™s probably an element of different answers coming from different needs: people who work with small apps donā€™t stray far from The Rails Way of MVC (models, views, and controllers), whereas those who work with larger apps might feel the need for a more sophisticated architecture.

That being said, I sense that these disagreements also reflect a more fundamental question: How should the app interact with the database? Or in other words, should database tables be near the surface, or should we put in the effort to hide the data model that is reflected in database tables?

I may have lost you already, so before I wade too deep into philosophy, let tell the story of why Iā€™m struggling with these questions.

The good old days

Before I learned Rails, I knew Ruby. I loved it. It made sense. Propelled by Sandi Metzā€™s talks and books, I could write a plain Ruby app in the most beautiful and satisfying OOP style. Life was good.

But I knew I couldnā€™t linger in those enchanted woods forever.

Then along came Rails

I learned Rails and got my first programming job working on a Rails app of over two hundred thousand lines of Ruby code, plus React views. Suddenly things didnā€™t make so much sense anymore. I often didnā€™t (and still donā€™t) know where a piece of code belongs. Letā€™s even set aside React views and the duplication of backend logic that I find hard to resist when writing a React view. Letā€™s focus only on backend Ruby code: even there I find myself indecisive when trying to decide where to put a new piece of code.

The most convenient place for that new bit of code is an existing Active Record model, but when Iā€™m crawling through a huge model Iā€™m reminded that maybe I should think hard about where to put this code. So I turn to alternative places, but then Iā€™m faced with a jungle of service objects and variously-located POROs šŸ˜µā€šŸ’«

I usually find a tolerable solution, but in the end I always wonder: where does business logic really belong? šŸ¤”

Two philosophical camps?

As I looked through discussions of this question in the Ruby community, I noticed that most answers came from one of two ā€œsidesā€: advocates and opponents of service objects. In reality itā€™s a bit more nuanced than that: advocates might propose a pattern that is a more sophisticated version of service objects, and many opponents admit that careful OOP design is important to augment Railsā€™ MVC structure.

But the reason I lump them into two camps is that each has a different approach to the fundamental question I posed earlier: How should the app interact with the database? In the context of Rails, this question can be rephrased like this: What should an Active Record model represent?

Advocates of service objects often think of Active Record models as models of database tables, and therefore not an appropriate place to put business logic. The other camp sees Active Record models as models of domain objects that just happen to be backed by a database table, and therefore a perfectly suitable place for business logic.

Service object skepticism

For several months I thought the anti-service-object camp was right, end of discussion. It seemed clear to me that Active Record models are intended to be domain models:

1. Itā€™s spelled out in the Rails Guides.

From the section ā€œWhat is Active Record?ā€(emphasis mine):

ā€œActive Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database.ā€

And, shortly afterward:

ā€œIn Active Record, objects carry both persistent data and behavior which operates on that data.ā€

2. Martin Fowler, who first described the Active Record pattern, agrees.

To quote his article on the Active Record pattern:

ā€œAn object carries both data and behavior. Much of this data is persistent and needs to be stored in a database. Active Record uses the most obvious approach, putting data access logic in the domain object.ā€

So an Active Record object is intended to be fundamentally a domain object, with database access added for convenience, not the other way around. Probably thatā€™s why it seems against the grain of Rails when service objects are the place where business logic goes.

Fowler directly criticizes service objects in his article on anemic domain models. In reference to putting domain logic in services, he says:

ā€œThe fundamental horror of this anti-pattern is that itā€™s so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design.ā€

And:

ā€œIn general, the more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model. If all your logic is in services, youā€™ve robbed yourself blind.ā€

3. Conversely, domain models donā€™t have to be Active Record models; they can be PORO models.

Taking advantage of this can alleviate many of the ā€œfat modelā€ problems that service objects seek to solve.

Martin Fowler proposes refactoring a service object into a PORO, and heā€™s not alone: some in the Ruby community have written the same (1, 2, 3).

There are lots of patterns that can be used in POROs around Active Record models. For example, if a record is created from complex form input, you could use a form object instead of a service object.

Also, some versions of service objects are somewhat object-oriented when they reject the notion that service objects should have only a #call method and when they share code within the same class. In these cases, a service object is a bit more like a purpose-built PORO.

So why not just take the next step and put these services in the app/models folder, and refactor them from procedures into actual domain models? To take an example from the last link above: SalesTeamNotifier.send_daily_notifications could be changed to Internal::Notification.new(receiver: 'sales').send.

So yeah, I was a convinced service object skeptic, firm in dismissing even the need for anything but classic OOP. When I tried to be fair and play devilā€™s advocate, I only got as far as conceding that OOP is harder to get right than procedures, and OOP done wrong can result in a lot of moving parts and less clarity about what actually happens when. I could even appreciate the simplicity of services, in the sense that making one is as easy as copy-pasting a long model method.

Second-guessing myself; more study needed

Fast forward a few months. I still donā€™t like service objects, and I still like OOP. But now Iā€™m less certain that the cure-all for badly organized business logic is ā€œjust do more OOP, end of story.ā€

After all, if so many people feel the need for service objects, and if OOP is evidently so hard to get right, arenā€™t these signs that something is missing? Maybe that missing something is just better OOP, but in that case good OOP is hard to come by and we at least need a more accessible way to do it.

So Iā€™ve set out to explore the problem of organizing business logic from more angles than before, using the resources listed below. These lists are excerpted from my ā€œLearning Rubyā€ road map which I often update, so you may want to find these lists there if this post is old at the time of your reading it. The sections corresponding to the lists below are, at the time of writing, ā€œRails architectureā€ and ā€œRails codebasesā€.

Deductive study: books, talks, and gems

Here are some resources that I hope will shed light on the question of organizing business logic better, both in terms of solutions and in terms of when (under what conditions) these alternative approaches are beneficial as opposed to simple OOP with Rails defaults. This list is not exhaustive; in particular Iā€™ve omitted gems that are just a service object implementation. Some of these resources are closely related to service objects, but thatā€™s intentionalā€“Iā€™m compensating for my bias against them.

Inductive study: open-source Rails codebases

I rarely read a lot of code outside of work, but I plan to change that. Below are Rails projects that Iā€™ve seen mentioned more than once as good examples to learn from, or they are sufficiently active and well-known as to be good candidates for study.

Conclusion: to be continuedā€¦

In a year or two I may be able to give something more like an answer to the questions Iā€™ve raised here. For now, Iā€™ve made a start by processing my thoughts and mapping out some promising resources. If any of this helps you as well, dear reader, then all the better!

šŸ‘‰ Next: Learning Ruby šŸ‘ˆ Previous: The first six months šŸš€ Back to top