Ruby

OO Design in Rails Where to put stuff

19 September 2026 · 12 min read

OO Design in Rails Where to put stuff

Object-oriented (OO) design principles are fundamental to building robust and maintainable Rails applications. However, even seasoned Rails developers sometimes grapple with a common question: where do you actually put stuff? Knowing where to organize your code—from models and controllers to service objects and concerns—is crucial for creating a clean, scalable, and testable application. A poorly organized Rails app can quickly devolve into a tangled mess of dependencies and duplicated code, making future development a nightmare. This article dives deep into best practices for OO design in Rails, providing practical guidance on structuring your application for long-term success. We’ll explore different approaches, discuss common pitfalls, and provide actionable strategies for keeping your codebase organized and maintainable.

Understanding the Rails Way (and When to Deviate)

Rails provides a strong convention-over-configuration framework that encourages developers to follow specific patterns. Models typically handle data persistence and business logic related to database records. Controllers manage the interaction between the user and the application, receiving requests, processing data, and rendering views. Views present the data to the user through HTML templates. While these conventions are a great starting point, they don’t always provide enough structure for complex applications. As your application grows, you’ll likely need to introduce additional layers of abstraction to keep your models and controllers lean and focused. This is where understanding object-oriented design comes into play.

One common pitfall is stuffing too much logic into models or controllers. This can lead to “fat models” and “fat controllers,” which are difficult to test, maintain, and reuse. For example, consider a model that handles both data validation and complex calculations. Instead of placing all this code directly within the model, consider extracting the calculation logic into a separate service object. Similarly, a controller shouldn’t be responsible for complex business rules or data manipulation; these tasks are better delegated to models or other dedicated classes. Understanding when to deviate from the standard Rails conventions is critical for building scalable and maintainable applications. Remember the principle of single responsibility: each class should have one, and only one, reason to change.

According to Martin Fowler, a renowned software development expert, “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” This quote underscores the importance of writing clean, well-organized code that is easy for other developers (and your future self) to understand and maintain. By adhering to OO design principles, you can create a Rails application that is not only functional but also a pleasure to work with. This involves thinking carefully about where responsibilities lie and designing classes that are cohesive, loosely coupled, and highly reusable. Employing techniques like dependency injection and interface-based programming can also contribute to a more flexible and testable codebase.

Service Objects: Encapsulating Business Logic

Service objects are a powerful tool for encapsulating complex business logic in Rails applications. They provide a clear and organized way to handle tasks that don’t naturally fit within models or controllers. A service object typically encapsulates a single, well-defined operation and is responsible for orchestrating the steps required to complete that operation. This approach helps to keep your models and controllers lean and focused, making your code easier to test and maintain. Service objects are a key part of proper object-oriented design in Rails.

For example, imagine you have a Rails application that handles order processing. The process of creating an order might involve several steps, such as validating the user’s payment information, creating the order record in the database, sending a confirmation email, and updating inventory levels. Instead of placing all this logic in the controller, you could create a CreateOrderService object that encapsulates these steps. The controller would simply call the service object, passing in the necessary parameters, and the service object would handle the rest.

Here’s an example of how you might structure a service object:

ruby app/services/create_order_service.rb class CreateOrderService def initialize(user, cart) @user = user @cart = cart end def call Validate payment information Create the order record Send a confirmation email Update inventory levels Handle any errors end end app/controllers/orders_controller.rb def create result = CreateOrderService.new(current_user, current_cart).call if result.success? Redirect to success page else Display error message end end By using service objects, you can create a more modular and maintainable Rails application. This approach makes it easier to test your business logic in isolation and reduces the risk of introducing bugs when making changes to your codebase. Remember that service objects are classes, and should follow the single responsibility principle. Learn more about object-oriented design and how it improves code quality.

Concerns: Sharing Code Between Models

Concerns are a mechanism in Rails for sharing code between models. They allow you to extract common functionality into reusable modules that can be included in multiple models. This can be particularly useful for handling tasks such as authentication, authorization, and data validation. However, it’s important to use concerns judiciously, as they can sometimes make your code harder to understand and maintain. Misuse of concerns can lead to implicit dependencies and decreased code clarity.

Consider a scenario where you have multiple models that need to be “taggable.” Instead of duplicating the tagging logic in each model, you could create a Taggable concern. This concern would define the methods and associations required for tagging, and you could then include this concern in any model that needs to be taggable. This approach helps to keep your models DRY (Don’t Repeat Yourself) and reduces the amount of code you need to maintain. Concerns promote code reusability and reduce redundancy.

Here’s an example of a Taggable concern:

ruby app/models/concerns/taggable.rb module Taggable extend ActiveSupport::Concern included do has_many :taggings, as: :taggable has_many :tags, through: :taggings end def tag_list tags.map(&:name).join(", “) end def tag_list=(names) self.tags = names.split(”,").map do |n| Tag.where(name: n.strip).first_or_create! end end end app/models/article.rb class Article < ApplicationRecord include Taggable end app/models/product.rb class Product < ApplicationRecord include Taggable end When using concerns, it’s important to ensure that they are well-defined and that they don’t introduce unnecessary dependencies between models. Avoid creating concerns that are too large or that contain unrelated functionality. A good rule of thumb is to keep concerns focused on a single, specific task. By using concerns effectively, you can create a more modular and maintainable Rails application. Remember that concerns should only be used for cross-cutting concerns, such as logging or auditing, and not for core business logic. Overusing concerns can create a “god module” anti-pattern, which should be avoided. Concerns should promote separation of concerns, not diminish it.

Value Objects: Representing Domain Concepts

Value objects are a design pattern used to represent domain concepts that have no identity. Unlike models, which are typically associated with database records and have a unique ID, value objects are defined by their attributes. They are immutable, meaning their state cannot be changed after they are created. This immutability makes them easier to reason about and less prone to errors. Value objects are useful for representing things like currency amounts, dates, and addresses.

For example, consider an application that deals with money. Instead of representing currency amounts as simple floating-point numbers, you could create a Money value object. This value object would encapsulate the amount and the currency, and it would provide methods for performing calculations such as addition, subtraction, and multiplication. By using a value object, you can ensure that currency amounts are always handled consistently and that you don’t accidentally introduce rounding errors. This promotes data integrity and prevents unexpected behavior. Value objects improve code reliability and reduce the risk of data corruption.

Here’s an example of a Money value object:

ruby app/models/money.rb class Money attr_reader :amount, :currency def initialize(amount, currency) @amount = amount @currency = currency end def +(other) raise ArgumentError, “Currencies must match” unless currency == other.currency Money.new(amount + other.amount, currency) end Add other methods for subtraction, multiplication, etc. def ==(other) amount == other.amount && currency == other.currency end def eql?(other) self == other end def hash [amount, currency].hash end end Value objects can also improve the readability and maintainability of your code. By encapsulating domain concepts in dedicated classes, you can make your code more expressive and easier to understand. When using value objects, it’s important to ensure that they are immutable and that they provide methods for performing any necessary calculations or transformations. This helps to ensure that your code is robust and reliable. Value objects represent a key component of domain-driven design, allowing you to model your application based on the real-world concepts it represents. By using value objects, you can improve the clarity and correctness of your code.

Structuring Your Rails Application: A Practical Guide

Structuring a Rails application effectively requires careful consideration of the different components and their responsibilities. While Rails provides a default directory structure, it’s often necessary to create additional directories and files to accommodate more complex logic. Here’s a practical guide to structuring your Rails application:

  1. Models: Place your models in the app/models directory. Each model should be responsible for managing data persistence and business logic related to a specific database table.
  2. Controllers: Place your controllers in the app/controllers directory. Controllers should be responsible for handling user requests and orchestrating the interaction between models and views.
  3. Views: Place your views in the app/views directory. Views should be responsible for rendering data to the user through HTML templates.
  4. Helpers: Place your helpers in the app/helpers directory. Helpers should be responsible for providing view-specific logic and formatting.
  5. Services: Create a app/services directory for service objects. Service objects should encapsulate complex business logic that doesn’t naturally fit within models or controllers.
  6. Concerns: Create a app/models/concerns directory for concerns. Concerns should contain reusable modules that can be included in multiple models.
  7. Value Objects: Create a app/models directory and subdirectory for value objects.

In addition to these standard directories, you may also want to create directories for other types of classes, such as form objects, presenters, and policies. The key is to organize your code in a way that makes it easy to find, understand, and maintain. Consider these points when designing your application structure:

  • Follow the principle of least astonishment: organize your code in a way that is predictable and consistent with Rails conventions.
  • Keep your directories and files organized: use meaningful names and avoid creating deep directory structures.
  • Document your code: use comments to explain the purpose of each class and method.

By following these guidelines, you can create a Rails application that is well-structured, maintainable, and easy to understand. Remember that the goal is to create a codebase that is easy for other developers (and your future self) to work with. A well-organized Rails application will save you time and effort in the long run.

Infographic here
FAQ ---
What are the benefits of using service objects in Rails?
Service objects promote separation of concerns, making your code more modular, testable, and maintainable. They encapsulate complex business logic, keeping your models and controllers lean and focused.
When should I use a concern in Rails?
Use concerns when you need to share code between multiple models. However, use them judiciously, as they can sometimes make your code harder to understand and maintain. Only use concerns for cross-cutting concerns, and not for core business logic.
What is a value object and when should I use it?
A value object is a design pattern used to represent domain concepts that have no identity, such as currency amounts, dates, and addresses. Use value objects to encapsulate these concepts and ensure that they are handled consistently throughout your application.
How can I improve the structure of my Rails application?
Follow Rails conventions, create directories for different types of classes (e. **Question & Answer :** I'm really enjoying Rails (even though I'm generally RESTless), and I enjoy Ruby being very OO. Still, the tendency to make huge ActiveRecord subclasses and huge controllers is quite natural (even if you do use a controller per resource). If you were to create deeper object worlds, where would you put the classes (and modules, I suppose)? I'm asking about views (in the Helpers themselves?), controllers and models.

Lib is okay, and I’ve found some solutions to get it to reload in a dev environment, but I’d like to know if there’s a better way to do this stuff. I’m really just concerned about classes growing too large. Also, what about Engines and how do they fit in?

Because Rails provides structure in terms of MVC, it’s natural to end up using only the model, view, and controller containers that are provided for you. The typical idiom for beginners (and even some intermediate programmers) is to cram all logic in the app into the model (database class), controller, or view.

At some point, someone points out the “fat-model, skinny-controller” paradigm, and intermediate developers hastily excise everything from their controllers and throw it into the model, which starts to become a new trash can for application logic.

Skinny controllers are, in fact, a good idea, but the corollary–putting everything in the model, isn’t really the best plan.

In Ruby, you have a couple of good options for making things more modular. A fairly popular answer is to just use modules (usually stashed in lib) that hold groups of methods, and then include the modules into the appropriate classes. This helps in cases where you have categories of functionality that you wish to reuse in multiple classes, but where the functionality is still notionally attached to the classes.

Remember, when you include a module into a class, the methods become instance methods of the class, so you still end up with a class containing a ton of methods, they’re just organized nicely into multiple files.

This solution can work well in some cases–in other cases, you’re going to want to think about using classes in your code that are not models, views or controllers.

A good way to think about it is the “single responsibility principle,” which says that a class should be responsible for a single (or small number) of things. Your models are responsible for persisting data from your application to the database. Your controllers are responsible for receiving a request and returning a viable response.

If you have concepts that don’t fit neatly into those boxes (persistence, request/response management), you probably want to think about how you would model the idea in question. You can store non-model classes in app/classes, or anywhere else, and add that directory to your load path by doing:

config.load_paths << File.join(Rails.root, "app", "classes") 

If you’re using passenger or JRuby, you probably also want to add your path to the eager load paths:

config.eager_load_paths << File.join(Rails.root, "app", "classes") 

The bottom-line is that once you get to a point in Rails where you find yourself asking this question, it’s time to beef up your Ruby chops and start modeling classes that aren’t just the MVC classes that Rails gives you by default.

Update: This answer applies to Rails 2.x and higher.