Skip to Content
Course content

81: Rails MVC Architecture In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Where exactly should the "heavy lifting" logic live?

You've probably heard the mantra "Skinny Controller, Fat Model." When I first started with Rails, I took that literally and shoved every single piece of business logic into my ActiveRecord models. I thought I was being a "good Rails developer," but I ended up with models that were 2,000 lines long and impossible to test without mocking the entire database.

Here is the reality: Models should handle data integrity and basic associations. Controllers should only handle request routing and response orchestration. If you have a process that involves multiple models, external APIs, or complex conditional flows—like enrolling a student in a course, charging their credit card via Stripe, and sending a welcome email—that doesn't belong in either.

In these cases, I use a Service Object. It's just a plain old Ruby class that does one thing. Look at this contrast:

# The "Fat Controller" way (Avoid this)
def create
  @enrollment = Enrollment.new(enrollment_params)
  if @enrollment.save
    PaymentGateway.charge(current_user, @enrollment.price)
    UserMailer.welcome_email(@enrollment.user).deliver_later
    redirect_to @enrollment, notice: 'Enrolled successfully!'
  else
    render :new
  end
end

# The "Service Object" way (Do this)
def create
  result = CourseEnrollmentService.call(user: current_user, params: enrollment_params)
  
  if result.success?
    redirect_to result.enrollment, notice: 'Enrolled successfully!'
  else
    @errors = result.errors
    render :new
  end
end


By moving that logic into `CourseEnrollmentService`, your controller stays focused on HTTP concerns, and your service object can be tested in total isolation from the web request.

How does the Controller actually "talk" to the View?

It feels like magic, but it's actually just Ruby's instance variable scope. When a Rails controller action finishes executing, Rails implicitly renders the view template associated with that action. Any instance variable (the ones starting with @) defined in the controller action is automatically available in the corresponding view.

If you define @courses = Course.all in your index action, the index.html.erb file can just loop through @courses. But here is a tip I learned the hard way: don't use the view to "find" data. I've seen beginners do this in their HTML:

# In a view file (Bad practice)
<p>Instructor: <%= @course.instructor.user.full_name %></p>

This creates an "N+1 query" nightmare. The view should be "dumb." It should simply display the data the controller has already prepared. If you need a specific string formatted for the UI, calculate it in the controller or a helper method, and pass the final result to the view.

Why can't I just put a database query inside the View?

Technically, you can. Ruby is Ruby, and the view is just executed in a context where the Model classes are available. But you absolutely shouldn't. If you start putting Course.where(active: true) inside your .html.erb files, you've broken the MVC boundary.

Why do I care so much about this? Two reasons: Testing and Maintenance. If your data fetching is buried in the HTML, you can't write a unit test for that logic without rendering the entire page. Plus, if you ever decide to change how an "active" course is defined (maybe now it needs a published_at date in the past), you'll have to hunt through a dozen HTML files to find every place you wrote that query.

Keep the View as a presentation layer. Its only job is to take the variables the Controller gave it and turn them into HTML. If you find yourself writing complex logic in a view, it's a signal that you either need a better Controller action or a dedicated Helper method.




📋 Practical Task

Refactor the Bloated Enrollment Controller

You have a CourseEnrollmentsController where the create action is doing too much: it validates the user's subscription, creates the enrollment record, updates the course's student count, and sends a confirmation email.

Your Task: Create a Service Object named EnrollmentProcessor in app/services/enrollment_processor.rb. Move all the business logic (subscription check, record creation, count update, and emailing) out of the controller and into a .call method in this service. Update the controller action to call this service and handle the success or failure response using a redirect or a render.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.