Master your next Ruby on Rails interview with our comprehensive collection of questions and expert-crafted answers. Get prepared with real scenarios that top companies ask.
Prepare for your Ruby on Rails interview with proven strategies, practice questions, and personalized feedback from industry experts who've been in your shoes.
Thousands of mentors available
Flexible program structures
Free trial
Personal chats
1-on-1 calls
97% satisfaction rate
Choose your preferred way to study these interview questions
To create a new Rails application, you use the rails command followed by new and the name of your application. For example, rails new myapp. This command generates a new Rails app with a default directory structure and takes care of all the initial setup needed to get started. You can also add options to customize the setup, like skipping the test framework with --skip-test or using a specific database with -d postgresql.
Helpers in Rails are designed to keep your views clean and DRY (Don't Repeat Yourself). They allow you to extract complex logic out of the view templates and into reusable methods. This makes your code more readable and maintainable. You typically define these methods in helper modules, which are included automatically in your views. So, whenever you have something that you need to use in multiple views, like formatting dates or creating certain HTML structures, you put that logic into a helper.
In Rails, management of database relationships is relatively straightforward thanks to a library called ActiveRecord. ActiveRecord is an Object-Relational Mapping (ORM) system, which means it handles the transfer of data between Ruby objects and database tables. This allows you to use Ruby syntax to perform database operations rather than SQL.
When setting up relationships, ActiveRecord provides methods for four key types of relationships between database tables: 'belongs_to', 'has_one', 'has_many', and 'has_many_through'.
For example, if you have Users and Posts in a blog application, you could say a User 'has_many' Posts, and a Post 'belongs_to' a User. This would set up a one-to-many relationship between Users and Posts.
The key is to properly define these relationships in your model files. By doing so, you can then perform complex database queries using simple Ruby methods, without needing to manually write SQL queries. This makes manipulating and accessing related data more intuitive and less error-prone.
Try your first call for free with every mentor you're meeting. Cancel anytime, no questions asked.
Rails' I18n (short for internationalization) feature allows you to translate your application into multiple languages. It provides a framework to store and retrieve translations, and it comes in handy if your application needs to be localized for different user groups based in different countries.
At the basic level, managing translations with I18n involves defining translations in locale files (config/locales/*.yml), and then fetching these translations in your views, models and controllers.
For example, you could have two locale files, en.yml (for English) and es.yml (for Spanish). Inside en.yml, you'd define:
yml
en:
hello: "Hello world"
And inside es.yml, you'd define:
yml
es:
hello: "Hola mundo"
Then in any view, you can use the t (or translate) helper method to retrieve the translation:
erb
<%= t('hello') %>
Depending on the current locale (I18n.locale), this will render "Hello world" or "Hola mundo".
To switch locales, you can set I18n.locale to any of the available locales at the beginning of each request, possibly by taking it from the user's preferences, the Accept-Language HTTP header, or the domain name.
That's the essence of it. Rails' I18n is quite feature-rich and allows you to do a lot more than this, such as defining translations in Ruby files, providing default translations, and using variables and pluralization in translations. But for many applications, this simple pattern is enough for most of your needs.
I’d explain it in two parts, what they are, and when you’d use each one.
String is for text content.Symbol is more like an internal label or identifier.Key differences:
Symbols are immutable, once created, they do not change.
Object identity
The same symbol name points to the same object.
Typical use
Example:
"status" is a string, useful if you need to manipulate the text.:status is a symbol, useful if you just need a stable key or label.Why it matters:
A simple Rails example:
params[:id], render :show, and belongs_to :user all use symbols because those values are identifiers, not editable text.The bundle exec command in Ruby is used to run a specific command in the context of the current bundle. A bundle, as defined by Ruby's Bundler, is the complete set of gems that your application needs to run, as specified in your Gemfile.
The command bundle exec restricts the command following it (like rails server or rake db:migrate) to the versions of the gems specified in the Gemfile.lock file. This ensures that the commands are executed using the correct versions of the gems, avoiding potential conflicts between different versions of the same gem.
This becomes particularly important in situations where you might have global gems installed on your system that are different versions than the ones your application requires. If you were to simply run rails server, Ruby would use the globally installed gem. But with bundle exec rails server, it runs the command with the version of Rails listed in your Gemfile.lock, preventing potential issues caused by version conflicts. So, it's a good practice to always use bundle exec for running rake tasks, Rails servers, tests or any other command that uses your application's gems.
Both 'belongs_to' and 'has_one' are types of associations that you can define between two models in Rails. They both establish a one-to-one connection with another model but are applied depending on the relationship between those models.
'belongs_to' is used when the foreign key resides in the model declaring the association. For example, if you have a User model and a Profile model, where every user has one profile and the profiles table has a user_id foreign key, the associations would look like this:
```ruby class User < ApplicationRecord has_one :profile end
class Profile < ApplicationRecord belongs_to :user end ```
On the other hand, 'has_one' is used when the other model contains the foreign key. In the above scenario, User model has_one Profile.
So in essence, 'belongs_to' and 'has_one' both set up a one-to-one connection but 'belongs_to' is used where the foreign key is, while 'has_one' is used on the other side of the relationship.
Modules in Ruby serve two main purposes: namespace encapsulation and mix-in functionality.
Namespace encapsulation is where you use a module to contain related classes, other modules, or methods, without putting them directly in the global scope. This helps prevent naming collisions. Here's an example:
``` module MyModule class MyClass def say_hello puts "Hello" end end end
MyModule::MyClass.new.say_hello # outputs "Hello" ```
The second purpose is to bundle related methods which can then be included into other classes, behaving as a kind of "mixin". Module methods can be mixed into classes using the include keyword. For example:
```ruby module Greetings def say_hello puts 'Hello!' end end
class MyClass include Greetings end
MyClass.new.say_hello # outputs "Hello!" ```
By including the Greetings module, MyClass now has access to its methods. The cool thing about this is that you can include the same module into multiple classes, offering a degree of code reusability.
Remember, unlike classes, modules can't be instantiated or inherit from one another; their sole purpose is to serve as a container or to be included in classes.
Get personalized mentor recommendations based on your goals and experience level
Start matchingRuby on Rails uses the MVC (Model-View-Controller) architectural pattern. This architecture separates an application into three interconnected components, enabling more structured and easy-to-manage code.
The 'Model' part corresponds to all the data-related logic. An ActiveRecord model interacts with the database and participates in handling data, including validations, relationships, transactions, and more.
The 'View' is all about what the user sees and interacts with – the user interface. In Rails, views are typically HTML files with embedded Ruby code that will manipulate and present data.
The 'Controller' serves as the middle-man between the Model and View. It handles the user's request, interacts with the model to fetch or manipulate data, and then chooses the right view to send that data to for presentation to the user.
In a typical Rails interaction, a user interacts with the View, which then raises a request handled by the Controller. The Controller processes the request, interacts with the Model if data manipulation is needed, and then serves a new View back to the user.
I usually debug Rails in layers, from fastest signal to deepest inspection.
My approach is:
In practice, that looks like this:
Logs first
Rails logs usually tell you a lot, request params, SQL queries, timings, exceptions, and stack traces. I’ll also add targeted log lines with Rails.logger.debug or Rails.logger.info if I want to trace a specific branch of logic.
Interactive breakpoints
If logs are not enough, I’ll drop in byebug or binding.pry and inspect the state directly. That helps me check params, model attributes, return values, and whether my assumptions are actually true.
Rails console
I use rails console a lot to recreate the problem outside the request cycle. It is great for testing queries, callbacks, scopes, service objects, and edge cases without clicking through the UI every time.
SQL and ActiveRecord inspection
A lot of bugs come down to unexpected queries or missing associations, so I check generated SQL, eager loading, nil values, and validation failures. If needed, I’ll call methods like valid?, errors.full_messages, or inspect the relation step by step.
Tests to narrow it down
If the bug is tricky, I’ll write or update a spec that reproduces it. That gives me a tight feedback loop and helps make sure the fix sticks.
Browser and network tools
For frontend-related Rails issues, especially with Hotwire, Turbo, or JSON APIs, I’ll also inspect the browser console, network tab, and response payloads.
A concrete example:
I had a case where a form submission looked successful, but the record was not being updated.
Here is how I worked through it:
updateSo overall, I start simple, logs and reproduction, then move to breakpoints, console, and tests until I can pinpoint the exact failure.
I usually think about Rails assets in three buckets:
In a typical Rails app:
app/assets is for app-specific assetslib/assets is for shared or custom stuffvendor/assets is for third-party files you want to keep in the repoFor styles, scripts, fonts, and images, I rely on Rails conventions and helpers so paths stay clean and fingerprinting works automatically.
A practical way to explain it in an interview is:
My answer would be:
Rails has traditionally used the asset pipeline to manage CSS, JavaScript, images, and fonts. It gives you a clean way to organize assets, compile them, and serve optimized versions in production.
In development, assets are easy to work with and usually loaded in a way that makes debugging simple.
In production, Rails precompiles them, adds digested filenames for cache busting, and compresses them so browsers can cache aggressively without serving stale files.
A few things I usually pay attention to:
app/assetsstylesheet_link_tag, javascript_include_tag, and image_tag so Rails resolves the right pathsIf the app is on newer Rails, I also check whether it uses:
jsbundling-railscssbundling-railsThat matters because "handling assets in Rails" can mean different setups depending on the app.
For example, on one project we had Sprockets handling images and CSS, but JavaScript was bundled separately. My job was mostly making sure deploys precompiled correctly, assets were fingerprinted, and CDN caching behaved properly. Most of the work was less about writing asset code, and more about making the pipeline predictable in production.
The 'yield' keyword in Ruby is used in the context of blocks. It's a way to inject code into a method from the "outside." Whenever Ruby encounters the 'yield' keyword in a method, it pauses execution of the method, runs the code in the block, and then resumes execution of the method. This allows sections of the method to be customized by whoever is calling it, without having to rewrite or modify the method itself.
For example, consider this code:
```ruby def custom_greeting puts "Hello, " yield puts "!" end
custom_greeting { print "world" } ```
The 'yield' keyword in the 'custom_greeting' method allows the 'print "world"' line to be injected into the method, producing the output "Hello, world!". Always remember though, if you include a 'yield' statement, you must provide a block when calling the method, or else Ruby will raise a LocalJumpError.
Ruby Gems are packages of code that add functionality to your Ruby projects. They serve to extend or modify the functionality in Ruby applications. Gems can range from tiny libraries that provide a single function, to large frameworks like Rails.
Using a gem is simple. First, you find a gem that provides the functionality you need, usually by searching on RubyGems.org, the main repository of Ruby gems. Once you've found a gem to use, you can install it with the gem install command.
However, in modern Ruby applications, it's common to use Bundler to manage gems. Bundler ensures that the correct versions of each gem - and its dependencies - are used. To use Bundler, you add the name of the gem to your Gemfile, which is a list of all the gems your project needs, optionally specifying a version number. Then, you run the bundle install command, which installs all the gems listed in the Gemfile.
Once the gem is installed, you can use its functionality by requiring it at the top of your Ruby file with require 'gemname'. Then, you can start calling the methods or classes that the gem provides. Each gem has its own set of functionalities and ways of implementation, so refer to each gem documentation for the specific usage.
ActiveRecord is the default Object-Relational Mapping (ORM) layer supplied with Ruby on Rails. It abstracts and simplifies database operations. Instead of manually writing SQL queries, you can work with your data in terms of Ruby objects and methods, ActiveRecord translates those into the appropriate SQL queries under the hood.
ActiveRecord models in a Rails application represent database tables and are the place where data logic resides. For instance, if you have an User model, you'd potentially have a corresponding 'users' table in the database, and each instance of the User model represents a row in that database table.
ActiveRecord provides a ton of functionality out of the box, including but not limited to CRUD operations (Create, Read, Update, Delete), data validation, querying capabilities, and handling of relationships between different models.
For example, if you want to find all users with the first name "John", instead of writing SQL, you'd use ActiveRecord like this: User.where(first_name: "John"). ActiveRecord turns this into the appropriate SQL, sends it to the database, and gives you the result as User objects.
So, ActiveRecord essentially serves as a bridge between your Ruby code and the database, allowing you to interact with your data in a more intuitive, Ruby-ish way.
In the context of Ruby on Rails, RESTful routes are a way of organizing your app's routes around the REST (Representational State Transfer) architecture. REST is a set of conventions for building HTTP services, often used for APIs. It structures interaction around resources, which are any kind of object, data, or service that can be accessed by the client.
A RESTful route is a route that provides mapping between HTTP verbs (get, post, put, delete, patch) to controller CRUD actions (create, read, update, delete). Instead of thinking of routes in terms of pure URLs, it is more accurate to think of them as routes to specific resources.
To put this into practice in Rails, you use resource routing. This provides a mapping between the HTTP verb, the URL, and the method to be called on the controller. For example, a GET request to /photos might map to the index action in the PhotosController, while a POST request to /photos maps to the create action.
By adhering to these conventions and using resource routing, your Rails application will by default have a RESTful design. This makes it easier to reason about, easier to design, and more amenable to being used as an API down the line, if necessary.
In Rails, you would create a route by defining it in the config/routes.rb file. Rails routes are essentially the mapping between HTTP requests to controller actions.
For example, to create a route for viewing a blog post, you might add the following line in your routes.rb file:
get 'posts/:id', to: 'posts#show'
This tells Rails to direct a GET request for 'posts/:id' to the 'show' action of the Posts controller. The ':id' portion is a dynamic segment that will match any number, and that value will be stored in params[:id].
Rails also offers resources keyword that can simplify route creation. For example, calling resources :photos in your routes file would create a full set of RESTful routes for a Photos resource, handling common actions like 'show', 'edit', 'update', 'destroy', and so on. This is a convenient way to map a large number of common routes at once.
Remember to run rake routes (or rails routes in newer versions) in the terminal after defining new routes to ensure they've been set up correctly and see a list of all routes in your application.
In Rails, render and redirect_to are two methods used in controller actions, but they perform quite different tasks.
render is used when you want to show a view to the user. It takes a template name, and Rails will look for that template in the corresponding view folder and show it. For example, render :show will display the 'show' template. This does not create a new request; it simply tells Rails what view to use in the current request.
On the other hand, redirect_to causes a new HTTP request. It tells the browser to send a new request to the route provided. It's like telling a user to visit another URL, maybe because the page they requested requires different parameters or they don't have permission to see it.
For example, after creating a new record, you might redirect_to @record, which will send the user to the 'show' page for the newly created record. Note that unlike render, redirect_to does not stop the execution of the function, so you'll often see a return statement following it, or it will be the last line of a function.
In short, render displays a view as part of the current request, while redirect_to triggers a new request to a different route. Both have their uses depending on what flow you need for your application.
A clean way to answer this is:
return behavior.Here’s how I’d say it:
A block is just an attached chunk of code you pass into a method. A proc is that idea turned into an actual object.
The practical difference:
Proc.Example:
array.each { |n| puts n }printer = Proc.new { |n| puts n }, then printer.call(1)One important detail is method signatures:
&block, which gives you a proc.Another difference interviewers usually want to hear is control flow:
return behavior, because return tries to exit from the surrounding method context.return more like a regular method.So the short version is:
If I wanted to sound especially practical in an interview, I’d add:
each, map, or selectRails sessions are basically a way to keep a little bit of state between requests.
The usual flow is:
session hashBy default, Rails uses CookieStore.
What that means:
In practice, you usually store small pieces of data, not whole objects.
Common example:
session[:user_id] = user.idsession[:user_id]A few important details:
user_id, instead of full recordsRails can also use other session stores if needed, like cache or database-backed approaches, but cookie-based sessions are the standard default for most apps.
Both save and save! methods in Rails are used to save the record to the database, but they behave differently when the record is invalid.
The save method returns a boolean value. If the record is valid and gets saved successfully in the database, it returns true. However, if the record is not valid, then it won't save the record in the database and it will return false. This method doesn't raise any exception on failure.
The save! method, on the other hand, will raise an ActiveRecord::RecordInvalid exception if the record is not valid. This can be useful when you want to ensure that the record must be saved, and if it isn't, the exception will alert you about it.
So if you want to check whether the record was saved or not and handle this manually, use save. If you want an exception to be raised on failure, use save!. It's important to use them appropriately based on the level of strictness you want to enforce and how you want to handle potential failures.
A before_action in Rails is a filter that is applied before certain controller actions are triggered. It's a type of callback that allows you to specify a method (or methods) which should run before the designated actions take place.
This is useful for checking preconditions and ensuring necessary conditions are met before an action is run. You might use a before_action to verify a user is authenticated or to find a record in the database that the action will interact with.
Here's an example. Assume you have a PostsController with an edit action that lets a user edit a post. Before allowing the user to edit, you want to make sure they're logged in. This could be done with a before_action like this:
```ruby class PostsController < ApplicationController before_action :authenticate_user!, only: [:edit]
# ...
def edit # code for editing the post end
private
def authenticate_user! redirect_to new_session_path unless logged_in? end
def logged_in? # return true if user is logged in, false otherwise end end ```
In this code, the authenticate_user! method is called before the edit action. If the user is not logged in, they're redirected, and the edit action is never triggered. The only option is used to specify that this before_action applies only to the edit action. Similarly, you can use except to specify actions where the filter should not apply.
Metaprogramming in Ruby is basically code that writes code, or changes behavior at runtime.
Ruby is really good at this because classes are open, methods are just objects Ruby can work with, and you can define behavior dynamically.
A simple way to explain it in an interview:
Examples I usually bring up:
define_methodUseful when you want to avoid repeating the same method pattern over and over
method_missing
If you use it, you should usually pair it with respond_to_missing?
Reopening classes or modules
In Rails, Active Record is the classic example.
name column, Rails gives you methods like name, name=, and query helpers dynamicallySo my short version would be:
"Metaprogramming in Ruby means writing code that defines or changes behavior dynamically at runtime. Ruby makes this easy with features like define_method, method_missing, and open classes. In Rails, Active Record uses metaprogramming all the time to generate attribute methods, association methods, and query helpers based on your model and schema. It is powerful, but I try to use it carefully so the code stays readable and easy to debug."
Rails helpers are methods that provide a way to extract complex logic out of views and keep the views clear of logic and calculations. These methods are available throughout your view templates, and help you abstract recurring patterns.
For example, suppose your application has a specific date format it needs to display. Instead of doing the date formatting in your view, you could write a helper method:
ruby
def formatted_date(date)
date.strftime("%A, %d %B %Y")
end
And then use it in your view like so:
erb
<%= formatted_date(@user.created_at) %>
Typically, your helpers would go inside a file in the app/helpers directory. By default, Rails creates a helper module for each controller in your application.
In addition to your own custom helpers, Rails provides a set of built-in helper methods that assist with tasks such as creating forms, outputting HTML, and managing text.
A key point to remember is that views should remain as logic-free as possible. They should focus on presenting information. When you feel that logic is creeping into your views, that's often a good time to write a helper.
Rack is a Ruby package that provides a minimal, modular, and adaptable interface for developing web applications in Ruby. Middleware in Rack represents a series of components that each accept a request, do something with it, and then either pass it along to the next middleware component or deliver a response back to the user.
Each piece of middleware is like a small filter or operation that the request goes through. This could be for logging, setting up sessions, handling cookies, parsing query parameters, or many other tasks.
In Rails, Rack middleware forms a stack, with the Rails application at the bottom. When a request is received by a Rails application, it is first processed by the Rack middleware at the top of the stack, which then interacts with the next middleware, and this process continues until the request reaches the Rails application.
You can see the middleware stack of a Rails application with the command rake middleware in your terminal. This will give you the list of all the middleware being used by your application, in the order they are called.
A key advantage of Rack middleware is that it allows for reusability of various web components, and it lets you insert, remove, or reorder components as needed, which gives you complete flexibility over how requests are handled in your application.
The respond_to block in Rails controller actions is used to handle different types of responses that can be requested by a client, depending on the format of the data it needs. It's used in conjunction with format methods like html, json, xml and others to define how the action should respond to each type of format.
For example, you might have a UsersController with a show action that could handle both HTML for web browser requests and JSON for API requests:
```ruby def show @user = User.find(params[:id])
respond_to do |format| format.html # render show.html.erb format.json { render json: @user } end end ```
In the above case, if the request URL ends with .html or no format is specified, it will render the "show" view (show.html.erb). If the URL ends with .json, it will render a JSON representation of the user.
Note that the blocks for the format.html and format.json calls are optional. If you don't provide a block, Rails will use sensible defaults: for html, it will render the appropriate view, and for json, it will attempt to render a JSON view or fall back to to_json on the given object.
So, the respond_to block plays the important role of giving you control over the representation of your resources depending on the requested format.
Memcache in Rails is basically about keeping frequently used data in memory, so you avoid hitting the database or recalculating the same thing over and over.
The usual setup is pretty simple:
dalli gem, which is the common Ruby clientIn Rails, that usually means configuring something like config.cache_store = :mem_cache_store or using dalli, depending on the Rails version and setup.
Once that is in place, you use the normal Rails caching APIs. That is the nice part, your app code stays very Rails-y.
Common ways to use it:
Rails.cache.fetchA typical pattern is:
expires_in: 10.minutesRails.cache.fetchFor example, if generating dashboard stats is slow, I would cache it with something like Rails.cache.fetch("dashboard_stats:#{user.id}", expires_in: 15.minutes), then only run the query logic on a cache miss.
A couple of practical things matter too:
So in short, Memcache with Rails is mainly plug it in as the cache backend, then use Rails.cache and fragment caching to speed up hot paths in the app.
I use ActiveJob as the Rails-friendly layer for background work, anything I do not want blocking a web request.
Typical use cases: - sending emails - processing images or files - syncing with third-party APIs - generating reports - cleanup or retryable maintenance work
How I usually set it up:
rails generate job ProcessImageapp/jobsperform, I keep the job focused on one responsibilityExample structure:
- queue_as :default or a more specific queue like :mailers or :low_priority
- def perform(image_id)
- Load the record inside the job, then do the work
I prefer passing IDs, not full ActiveRecord objects, because it is safer and avoids serialization issues.
perform_later when I want it asyncperform_now mostly in development, debugging, or very specific flowsFor example:
- after a user uploads an image, call ProcessImageJob.perform_later(image.id)
- the request returns quickly, and processing happens in the background
Common options: - Sidekiq, my usual choice in production - Delayed Job - Resque - Solid Queue, if staying close to the Rails ecosystem
Then I configure config.active_job.queue_adapter for the environment.
A couple of practical things I pay attention to:
- make jobs idempotent, so retries are safe
- use retry_on or discard_on for expected failure cases
- separate queues by priority
- keep heavy business logic in service objects, the job should orchestrate, not do everything
- log enough to debug failures easily
If I were answering this in an interview, I would structure it like this: - what ActiveJob is - when to use it - how you create and enqueue jobs - what backend you pair it with - one or two best practices from real projects
Concrete example:
- In a Rails app with file uploads, I used ActiveJob with Sidekiq to process images after upload.
- The controller saved the record, then queued ProcessImageJob.perform_later(image.id).
- The job loaded the image, generated thumbnails, stored metadata, and updated processing status.
- That kept the upload request fast and made retries easy if processing failed.
Cross-Site Request Forgery (CSRF) is a type of attack that tricks the victim into submitting a malicious request on behalf of the attacker. Rails includes built-in CSRF protection in the form of a security token that's added to each form that's generated.
This token is stored in the session and is included as a hidden field in all forms that are generated using Rails form builders. When the form is submitted, Rails checks the token from the form against the token stored in the session.
If the session token and the form token match, Rails accepts the request. If they don't match, Rails rejects the request with a 'Invalid Authenticity Token' error and doesn't execute the intended action. This protects against CSRF attacks because it ensures that only forms that have been generated by the app will be accepted.
This protection is enabled by default, but if you need to disable it for some reason (which is generally not advised), you can use skip_before_action :verify_authenticity_token in the relevant controller.
Also, it's good to know that this CSRF protection does not apply to APIs, because they are typically designed to be state-less, meaning that they don't have sessions. For API requests, you'd use different forms of authentication, like token-based authentication or JWT.
ActionCable is a built-in Rails framework that seamlessly integrates WebSockets with the rest of your Rails application. It allows for real-time features to be written in Ruby in the same style and form as the rest of your Rails application, while still being performant and scalable. It's a full-stack offering that provides both a server-side Ruby framework and a JavaScript client-side framework.
With ActionCable you can create features like chat, notifications, live updates, and everything else that needs real-time updates.
Here's a simple example for using ActionCable: Let’s say you're implementing a chat feature. First, you would need a channel. You can generate one with a command like this: rails generate channel Chat speak. This will create a 'ChatChannel' and a client-side JS file for the channel.
The speak method we've added here would be a server-side channel method that gets called by the client.
On the client-side, you can use JavaScript or CoffeeScript to manage interactions. This might include connecting to the channel, sending data, or receiving data:
javasript
App.chatChannel.speak(message);
You'll typically use ActionCable in combination with ActiveJob for broadcasting messages to the user in real-time. Any lengthy work would be handed off to ActiveJob to keep the WebSocket connection free.
ActionCable provides a real-time, highly interactive experience for users and it's a very powerful tool to have within the Rails environment.
"Convention over Configuration" in Rails means that the framework makes assumptions about what you're trying to do and sets up defaults, so you don't have to configure everything from scratch. For instance, if you have a model named Post, Rails will automatically look for a database table named posts. This reduces the number of decisions you need to make and the amount of code you have to write, streamlining development. Instead of spending time setting up configuration files, you can focus more on your application’s logic.
Active Record is the object-relational mapping (ORM) layer in Ruby on Rails. It abstracts away the details of database interactions, allowing developers to work with database records using Ruby objects and methods. Essentially, it maps tables in your database to Ruby classes, and table rows to instances of those classes. This makes it incredibly easy to create, read, update, and delete records without writing raw SQL queries, which speeds up development and makes your code more readable.
A polymorphic association in Rails allows a model to belong to more than one other model using a single association. This means you can have one model, say Comment, that can belong to both Post and Image models, instead of creating separate associations for each. For example, with polymorphic associations, you could have something like commentable that can refer to either a post or an image.
You would use a polymorphic association when you have a situation where multiple models share a common relationship. It helps to keep your database schema clean and avoids the need for multiple foreign keys for different types of associations. It's particularly helpful when you expect the number of associated models to increase and want to maintain flexibility in your codebase.
A migration is Rails’ way of changing the database schema in a tracked, repeatable way.
Think of it as version control for your database. Instead of manually editing tables in the DB, you describe the change in a migration file, and Rails applies it for you.
Common things migrations handle: - creating or dropping tables - adding or removing columns - changing column types - adding indexes - adding foreign keys
To create one, you usually use a generator from the command line.
For example:
- rails generate migration AddTitleToPosts title:string
That creates a migration file with the change already stubbed out.
Then you run:
- rails db:migrate
That applies the migration to your database.
A simple workflow looks like this:
1. Generate the migration
2. Review or edit the file if needed
3. Run rails db:migrate
4. Commit the migration with your code
Why it matters:
- everyone on the team can apply the same DB changes
- schema changes are tracked in the codebase
- you can roll changes forward and, in many cases, back with rails db:rollback
So in short, migrations are how Rails manages database structure safely and consistently over time.
Partials in Rails are a way to break down your view templates into smaller, reusable components. They're great for maintaining DRY (Don't Repeat Yourself) principles in your code. For instance, if you have a complex view that includes a form or a repetitive piece of HTML, you can extract that into a partial and include it wherever needed using <%= render 'partial_name' %>. This makes your main templates cleaner and easier to manage.
Turbo in Rails 7 is a part of the Hotwire framework, which aims to enhance the speed and interactivity of web applications without relying heavily on JavaScript. Turbo replaces the need for front-end frameworks by handling functionalities like page transitions, form submissions, and real-time updates directly from the server-side.
The major difference from previous versions is how Turbo simplifies app development by reducing the need for client-side code. Turbo Streams and Turbo Frames, for example, allow you to update parts of the DOM without a full page reload or tons of JavaScript. This integrates smoothly with the Rails architecture and enhances performance and responsiveness significantly.
In a Rails application, dependencies are primarily managed using Bundler. Your Gemfile is where you list all the gems your application needs. When you run bundle install, Bundler installs all the specified gems along with their dependencies and ensures they're available for your application. This process also creates a file called Gemfile.lock which locks the versions of the installed gems, ensuring that the same versions are used in different environments or by other developers working on the project.
The config/routes.rb file in a Ruby on Rails application is used to define the routes for your application. Essentially, it maps HTTP requests to specific controller actions. This file tells Rails how to handle incoming URLs and what code to execute when a specific path is requested. For example, you can set it so that a GET request to '/users' routes to the index action in the UsersController, or a POST request to '/users' routes to the create action in the same controller. This way, routes act as the bridge between the browser's requests and your application's responses.
I usually talk about testing in layers, starting with the stuff closest to the code, then moving outward.
A clean way to answer it is:
My approach is pretty pragmatic. I want fast, reliable tests that give confidence without turning into a maintenance burden.
In Rails, I’m comfortable with either Minitest or RSpec, but in practice I’ve used RSpec most often. I also like FactoryBot for test data and Capybara for end-to-end flows.
Here’s how I usually think about it:
If I have complex logic, I want that covered at this level because it’s fast and easy to debug
Request tests
For APIs, this is one of the biggest areas I focus on
System or feature tests
Capybara is useful here because it tests the app more like a real user would use it
Background jobs and mailers
Same for mailers, making sure the right emails are generated and sent
Non-functional checks
I also try to keep tests maintainable:
And I always wire tests into CI, usually GitHub Actions, CircleCI, or similar, so every PR runs the suite automatically. That helps catch regressions early and keeps the main branch stable.
If I wanted to make it more concrete, I’d say something like:
“In a Rails app, I usually build a test pyramid. Most coverage is around models, services, and request specs, because they’re fast and give strong confidence. Then I add a smaller number of system tests for the most important user flows. I’ve typically used RSpec, FactoryBot, and Capybara, and I make sure the whole suite runs in CI on every pull request.”
I usually think about Rails performance in layers, not just one trick.
A solid way to answer this is:
In Rails, the main techniques I’d call out are:
includes, preload, or eager_loadfind_each when it makes senseUse tools like Bullet, query logs, and EXPLAIN to spot slow queries
Caching
Make sure cache keys are predictable and invalidation is thought through
Background jobs
Sidekiq is a common choice, especially for higher-throughput apps
App and response optimization
Precompute or denormalize data if a read-heavy endpoint needs it
Infrastructure and runtime
Keep Rails and Ruby reasonably up to date, newer versions often bring real performance gains
Monitoring first
Example:
On one app, we had a slow index page that listed orders with customer and shipment info. The page felt fine in dev, but got slow in production as data grew.
I approached it like this:
includesThat brought response time down pretty noticeably, and just as importantly, reduced DB load. For me, that’s usually the Rails performance story, measure first, fix the biggest bottleneck, then verify the impact.
Callbacks in Rails are methods that get called at certain moments during an object's lifecycle, such as before or after it is created, updated, saved, or destroyed. They allow you to trigger logic automatically at these points, making them really handy for things like data validation, logging, or updating related objects. For example, you might use a before_save callback to normalize data before it's persisted to the database.
A concern in Rails is a way to modularize code, allowing you to encapsulate behaviors or functionalities that can be shared across multiple models or controllers. It leverages Ruby's mixin capabilities through modules. When you have methods that don't necessarily belong to a single model or controller but are used in multiple places, you can put these methods in a concern to keep your code DRY.
For instance, if you have a set of methods handling geolocation that needs to be used in several models, you can create a concern called Geolocatable and include it wherever necessary. This makes your code cleaner and more maintainable because you can modify the shared methods in one place.
RESTful routing in Rails is all about mapping HTTP verbs and URLs to controller actions in a consistent way that adheres to REST principles. It allows you to create easily understandable and predictable routes for your resources. For example, a resources :posts declaration in your routes.rb file will generate a standard set of routes like index, show, create, update, and destroy for the PostsController.
These routes map to the standard CRUD (Create, Read, Update, Delete) operations: GET for reading resources, POST for creating them, PATCH/PUT for updating them, and DELETE for removing them. This consistency makes it easier to understand and maintain the code, as you always know what URL structure and HTTP method to expect for any given action on a resource.
I’d answer this in two parts:
In Rails, I usually talk about background jobs through Active Job, then mention the queue backend, like Sidekiq, because that’s the most common setup.
A simple way to set it up:
sidekiq to the Gemfilebundle installThen I’d create a job, usually with something like:
rails generate job SendWelcomeEmailThat gives you a job class with a perform method. That perform method is where the async work goes, for example:
A typical job might look like:
class SendWelcomeEmailJob < ApplicationJobqueue_as :defaultdef perform(user_id)endThen to use it, I enqueue it from the part of the app that triggered the work. For example, after a user signs up:
SendWelcomeEmailJob.perform_later(user.id)That’s the key idea. The web request stays fast, and the heavier work runs in the background.
A few practical things I usually mention in interviews:
If they want a more direct Sidekiq example, you can also create a worker and enqueue with perform_async, but in most Rails apps I prefer Active Job as the interface and Sidekiq as the backend.
I usually answer this in two parts, approach first, then implementation.
DeviseOmniAuthhas_secure_passwordThe key is choosing the level of control vs speed. In interviews, I’d mention both, but make it clear what I’d pick by default.
Devise because it’s mature, secure, and saves a lot of time.Typical setup looks like this:
- Add devise to the Gemfile
- Run the install generator
- Generate the User model with Devise
- Run migrations
- Set up default mailer URLs per environment
- Add authentication guards like before_action :authenticate_user!
Then I’d configure only the modules I actually need, for example:
- database_authenticatable
- registerable
- recoverable
- rememberable
- validatable
For admin or high-risk actions, I’d consider MFA
If I need a custom solution
If the app has very specific requirements, I might avoid Devise and use bcrypt through has_secure_password.
In that case I’d implement:
- A password_digest column on users
- Login by verifying the password against the digest
- Session handling with session[:user_id]
- Helper methods like current_user
- Authorization checks on protected pages
That gives more flexibility, but I’d be careful because rolling your own auth means you’re responsible for all the security details.
authenticate_user!. If the app also needed Google or GitHub login, I’d layer in OmniAuth. If I needed a very custom flow, I’d use has_secure_password with bcrypt, store a password_digest, and manage sessions manually. The big thing is making sure password storage, session security, CSRF protection, and reset flows are handled correctly.”schema.rb is Rails’ current snapshot of the database structure.
A simple way to explain it:
Why it matters:
schema.rb without replaying every old migrationOne important detail, schema.rb is the end result, not the source of truth for how changes happened. The migrations are still what describe the step by step changes.
I try to handle exceptions at the right level, not just rescue everything and hope for the best.
A few rules I follow:
StandardError unless I really mean it.In Rails, that usually looks like this:
begin/rescue when I expect something like a third-party API timeout, parsing issue, or payment failure.rescue_from for app-level concerns like ActiveRecord::RecordNotFound or authorization errors.Example approach:
rescue_from in ApplicationController for consistent responses.A concrete example, if I am calling an external shipping API:
For common Rails exceptions, I usually set up things like:
ActiveRecord::RecordNotFound mapped to a 404The main thing is to treat exceptions as part of app design, not just error cleanup. The goal is predictable behavior for users and enough visibility for developers to fix issues fast.
render is used when you want to display a view template without making a new request. It can be used within the same action or after some logic. For example, if you have a form with errors, you might render the form view again to show those errors, while still maintaining the context of the original request.
On the other hand, redirect_to triggers a new HTTP request and tells the browser to navigate to a different URL. This is helpful when you want to follow the Post/Redirect/Get pattern to prevent duplicate form submissions or after successfully processing data and you want to take the user to a different page, like showing a list of items after creating a new one. Essentially, render keeps you in the same request-response cycle, while redirect_to initiates a new one.
Environment variables in a Rails application are typically used to keep sensitive information like API keys, database passwords, or configuration settings out of your codebase. To use them, you can either set them directly in your operating system or use a gem like dotenv-rails.
With dotenv-rails, you create a .env file in the root of your project and define your variables there, like SECRET_KEY=your_secret_key. Then, in your Rails app, you access these variables using ENV['SECRET_KEY']. This way you can keep these configurations out of your source control by adding the .env file to your .gitignore.
In Rails, handling file uploads can be efficiently managed using the Active Storage framework. Active Storage allows you to upload files to cloud storage services like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage, as well as to the local filesystem. You start by adding Active Storage to your application through a migration and setting up the necessary configuration. Then, you can attach files to your records using the has_one_attached or has_many_attached methods in your models.
For example, if you want to attach a profile picture to a User model, you would add has_one_attached :avatar to the User model. In your forms, you would include a file field for the attachment. Finally, you can use the avatar method to upload and retrieve the file in your controller and views. This simplifies the complexity around file uploads while providing a powerful and flexible way to manage them within a Rails application.
Active Job is a framework for declaring jobs and making them run on a variety of queuing backends in Rails. Essentially, it provides a standardized interface for background job processing. You'd use Active Job whenever you have tasks that are time-consuming or resource-heavy and can be performed asynchronously, such as sending emails, processing image uploads, or interacting with third-party APIs.
For example, if your app sends out welcome emails to new users, rather than making the user wait while the email is sent during the signup process, you can create a job for sending the email and enqueue it to be processed in the background. This improves the user experience by speeding up response times and handling tasks more efficiently.
Rails migrations are a convenient way to alter your database schema over time in a consistent and easy manner. When you create a migration, it generates a Ruby file with methods that define the changes to be made. You can add columns, create tables, drop tables, and even modify existing columns in your database.
To create a migration, you'd typically run a command like rails generate migration AddFieldToTable field:data_type. This creates a new migration file in the db/migrate directory. Inside this file, you'll have two methods: up and down. The up method defines what changes should be made to the database, and the down method defines how to reverse those changes, which is crucial for rolling back migrations.
To apply the migration, you run rails db:migrate, which will execute the code within the up method. If you need to reverse a migration, you can roll it back using rails db:rollback, which will execute the code in the down method. This structured approach to modifying the database schema helps ensure consistency and makes managing your database changes much simpler over the lifecycle of your application.
The after_commit callback in Rails is used to execute code after a record has been saved and committed to the database. It's helpful for tasks that should only run once the transaction is fully complete, like sending emails or updating external systems. You'd use it in your model like this:
```ruby class User < ApplicationRecord after_commit :send_welcome_email, on: :create
private
def send_welcome_email UserMailer.welcome_email(self).deliver_later end end ```
In this example, send_welcome_email will be called after a new user record is created and committed to the database. The on: :create option specifies that the callback should only run after a create action. You can also use :update, :destroy, or omit the on option to run the callback on any type of save operation.
In a Rails application, pagination can be efficiently handled using gems like Kaminari or WillPaginate. These gems provide helpers that make it incredibly simple to paginate your data sets. With Kaminari, for example, you just add the gem to your Gemfile and run bundle install. Then, in your controller, you can apply pagination to an ActiveRecord query like this:
ruby
@items = Item.page(params[:page]).per(10)
And in your view, you can add a pagination control with a single line:
erb
<%= paginate @items %>
This approach keeps your code clean and easily adjustable. You can customize the number of items per page, as well as the styling of the pagination controls, to fit the needs of your application and its users.
In Rails, session management is handled using the session hash, which stores data that you want to persist across multiple requests from the same user. By default, Rails uses a cookie-based session store where session data is stored on the client side inside a cookie that's signed and encrypted for security. You can access and modify session data using simple hash-like syntax, for example, session[:user_id] = @user.id.
For more advanced needs, you might opt for different session stores like ActiveRecord::SessionStore or Redis, especially when dealing with large amounts of session data or needing fast read/write access. Configuring these involves adding the appropriate gem, generating a session migration if needed, and updating your config/application.rb or config/initializers/session_store.rb to point to the new store.
strong_parameters is a feature introduced in Rails 4 to help prevent mass assignment vulnerabilities. By default, it requires developers to explicitly specify which parameters are allowed to be used in ActiveRecord models, which increases the security of the application. This ensures you're not unintentionally allowing users to set attributes that they shouldn't be able to, like admin status or user IDs.
To use strong_parameters, you typically define them in your controller by using the permit method inside a params.require block. For example, if you're dealing with a User model, you might have a private method in your UsersController like this:
ruby
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
This way, only the specified parameters (:name, :email, and :password) are allowed through, keeping your model safe from unwanted parameter changes.
Rails handles different environments using the config/environments directory, where you have separate files for development (development.rb), test (test.rb), and production (production.rb). These files allow you to define environment-specific configurations.
In addition to these files, Rails also uses the RAILS_ENV environment variable to determine which environment configuration to load. By default, Rails runs in the development environment, but you can switch to test or production by setting the RAILS_ENV variable accordingly when starting your server or running commands. This setup makes it easy to have different settings for things like database connections, logging levels, and email delivery methods, ensuring each environment is configured correctly without manual intervention each time.
You configure multiple databases in a Rails application by making use of the database.yml file. This file is located in the config directory of your Rails app. In Rails 6 and later, you can define multiple databases for different environments within this file. Each environment (like development, test, production) can have multiple database configurations specified under different names.
Here's a basic example for a development environment:
```yaml development: primary: database: my_primary_db username: user1 password: password1 host: localhost
secondary: database: my_secondary_db username: user2 password: password2 host: localhost ```
In your application code, you can then establish connections to the different databases using the configurations you've defined, typically managed through ActiveRecord.
Action Mailer in Rails is used to send out emails from your application. It helps you set up mailer views and templates just like you do for controllers and views, making it super easy to manage email-related functionalities. Think of it like the middleman between your app and the mail server.
Action Mailbox, on the other hand, is designed to receive emails into your application. It routes incoming emails to controller-like mailboxes for processing. So if you need to handle incoming mail, like replies to notifications or support emails, Action Mailbox is the feature you’d use.
Ruby on Rails is a web framework for building database-backed applications in Ruby.
At a high level, it gives you a strong default way to build things like:
Why it’s useful:
One of the biggest Rails ideas is convention over configuration.
That basically means Rails makes smart assumptions for you. If you follow the standard structure, a lot of things just work without extra setup. That speeds up development and makes apps easier for other Rails developers to jump into.
Another big benefit is maintainability.
Because Rails apps tend to follow consistent patterns, it’s easier for teams to collaborate, onboard new developers, and keep the codebase clean over time.
So if I had to describe Rails simply, I’d say:
It’s a framework that helps teams build web applications quickly, using sensible defaults, clean structure, and a lot of built-in tools.
I’d answer this in layers.
First, explain your testing strategy, not just the tools. Interviewers usually want to hear: 1. What you test. 2. At what level you test it. 3. How you keep tests fast and useful. 4. What tools you actually use in Rails.
My approach is usually:
In Rails, I’ve mostly used RSpec, but I’m comfortable with Minitest too. The framework matters less than having a solid test pyramid and keeping the suite reliable.
A typical setup for me looks like this:
I also like to keep tests practical:
For example, if I were building an e-commerce app, I’d test it like this:
POST /orders to verify auth, params, and response structure.I usually aim for confidence over raw coverage numbers. I’d rather have a smaller suite that catches real regressions than a huge suite full of brittle tests.
I’d answer this in two parts:
A solid answer would sound like this:
In Rails, I treat environments as separate runtime contexts with the same app code, but different configuration.
The default ones are:
development, optimized for local iterationdeveloper-friendly error pages
test, optimized for fast and repeatable automated tests
mailers, jobs, and external calls usually stubbed or faked
production, optimized for stability, security, and performance
In practice, I manage that through a few layers:
config/environments/development.rb, test.rb, and production.rbenvironment-specific Rails behavior lives there
credentials and environment variables
things like API keys, database URLs, and S3 config come from Rails credentials or the platform env vars
database config
test should never touch development or production data
service boundaries
letter_opener instead of sending real emails, and stub external APIs in testsI also try to keep environment differences intentional and minimal. If development behaves too differently from production, you end up with deployment surprises. So I like to mirror production where it matters, especially around caching, background jobs, and integrations.
For example, on a recent app:
development used SQLite locally at first, then Postgres once the app maturedtest had a completely separate Postgres database and mocked external APIsproduction used Postgres, Redis, Sidekiq, S3, and full error reporting through something like SentryIf the team needs it, I’m also fine creating custom environments like staging, usually to test production-like behavior before release. But I try not to create extra environments unless there’s a real operational need, because every new environment adds maintenance overhead.
I use ActiveRecord validations to put business rules as close to the data as possible.
A simple way to talk about it is:
Example:
If I have a User model, I might validate:
email is presentemail is uniquepassword meets a minimum lengthusername matches a formatIn Rails, that looks like using things like:
validates :email, presence: true, uniqueness: truevalidates :password, length: { minimum: 8 }validates :username, format: { with: ... }A few validations I use a lot:
presenceuniquenesslengthnumericalityinclusionformatA practical example:
On a signup flow, I would validate that the user has a unique email and a valid password before creating the account. If validation fails, Rails prevents the save and puts messages in errors, so the controller or form can show clear feedback to the user.
One thing I always call out, uniqueness validation in Rails is not enough by itself. For anything important, I also add a unique index at the database level to avoid race conditions.
So my rule of thumb is:
The Rails console is your app’s interactive sandbox.
It opens a Ruby session with the full Rails environment loaded, so you can work directly with:
You start it with rails console or just rails c.
How I use it in practice:
A few simple examples:
User.find(1) to inspect a recordUser.where(active: true) to test a queryUser.create!(name: "John") to create dataOrder.last.total_price to verify model logicIt’s especially useful when debugging. If something looks off in the app, I’ll jump into the console, pull the same records, run the same methods, and narrow down whether the issue is data, validations, callbacks, or query logic.
I also use it carefully in production. It’s powerful, so for production I’d stick to read-heavy investigation unless there’s a controlled reason to make a change.
I’d frame it around three things: what it is, what problems it solves, and what Rails is doing behind the scenes.
A clean answer would be:
The Rails asset pipeline is Rails’ way of organizing, processing, and serving front end assets like CSS, JavaScript, images, and fonts.
At a high level, it helps with a few important things:
app/assets, lib/assets, and vendor/assetsThe big win is performance and maintainability.
For example:
application-abc123.css, which makes cache busting easyIn production, assets are typically precompiled ahead of time, then served as static files by the web server or CDN.
One thing I’d mention in an interview is that the meaning of "asset pipeline" has shifted a bit across Rails versions. Traditionally this was handled with Sprockets. In newer Rails apps, JavaScript might be handled with tools like jsbundling-rails or import maps, while CSS might use cssbundling-rails. But the core idea is still the same, Rails gives you a structured way to manage and deliver frontend assets efficiently.
I’d explain Rails Engines as packaged chunks of Rails functionality.
They are basically mini Rails apps that live inside another Rails app, or can even be shared across multiple apps.
An engine can have its own:
The main use case is modularity.
If I have a feature that is big enough to stand on its own, or I want to reuse it in more than one app, an engine is a good fit.
A couple common examples:
In practice, you build the feature as an engine, package it cleanly, then mount it into a host app with its own route namespace.
So instead of scattering that logic all over the main app, you keep it isolated and easier to maintain.
Why teams use engines:
One important detail is that engines are not just plain gems. They are gems with Rails-specific structure and integration, so they plug into the framework much more deeply.
For example, if I had a company-wide admin feature used by several products, I’d likely put that into an engine. Then each app could mount something like /admin, run the engine’s migrations, and get the same behavior without rebuilding it each time.
Caching in Rails is basically about not doing the same expensive work over and over.
If something is slow to compute, like a query, a rendered partial, or a JSON response, Rails can store the result and reuse it until that data changes.
The main buckets are:
Fragment caching stores part of a page, not the whole thing.In views, you usually wrap a block with cache.
Low-level caching
Rails.cache.Example idea: cache a report result for 15 minutes instead of recalculating it on every request.
Collection caching
Rails can cache each item in a collection and reuse only the ones that did not change.
Russian doll caching
There are also older patterns like page caching and action caching, but in modern Rails, fragment and low-level caching are what you usually talk about first.
A big part of Rails caching is cache invalidation, meaning, when does the cached value get refreshed?
Rails helps with that through cache keys:
updated_at in their cache keyExample:
@product partial, Rails might use a key like products/123-20260316...You also choose a cache store, depending on the environment:
:memory_store for simple local setups:file_store for basic persistence:mem_cache_store with Memcached:redis_cache_store with Redis, very common in productionWhat I usually watch out for:
So in practice, Rails caching is, store expensive results, key them in a way that changes when the data changes, and use the right cache store for the app.
I’d keep it simple and lean on the Rails conventions.
A solid way to answer this is:
In Rails, I’d usually handle form-based auth with:
User modelhas_secure_password for password hashingSessionsController for login/logoutsession cookie to remember who’s logged inThe basic flow looks like this:
SessionsController#create, find the user by emailauthenticate(password)user.id in session[:user_id]destroyA few implementation details I care about:
bcrypt with has_secure_passwordcurrent_user in ApplicationControllerbefore_action like require_login for protected pagesSo in practice, I’d have:
new for the login formcreate to sign indestroy to sign outThen in ApplicationController, I’d add something like:
current_user, memoized from session[:user_id]logged_in?require_loginFor production, I’d also think about:
If the app needs more than basic username/password auth, I’d probably reach for Devise or Sorcery instead of building every detail myself. For a standard Rails app though, the built-in session approach is clean, reliable, and easy to maintain.
I’d answer this in layers, from most common to edge cases.
In Rails, the safest path is using things like:
where(name: params[:name])find_by(email: params[:email])User.where("email = ?", params[:email])Those all keep user input separate from the SQL itself, which is what prevents injection.
What I avoid is anything like:
"SELECT * FROM users WHERE email = '#{params[:email]}'"where("email = '#{params[:email]}'")That’s the classic way to create an injection hole.
If I need raw SQL, I still parameterize it. For example:
?A few practical rules I follow:
params directly into SQL strings.ORDER BY, column names, or table names, because parameterization does not protect those automatically.So the short version is, Rails gives you strong protection out of the box, as long as you use ActiveRecord the way it’s intended and treat raw SQL very carefully.
I’d explain it as a separation-of-responsibilities pattern.
In Rails, MVC keeps each part of the app focused on one job:
A simple way to think about it:
PostsController#indexPost.publishedWhat each layer does:
Talk to the database through Active Record
Views
In APIs, this might be JSON serializers or templates instead
Controllers
In practice, good Rails code usually means:
So the value of MVC in Rails is that it makes the app easier to read, test, and maintain because data logic, request handling, and presentation are kept separate.
I keep validations in the model when they represent business rules for that record, especially the stuff that should be true no matter where the data comes from.
My usual approach is:
presenceuniquenesslengthnumericalityinclusionformat
Add conditional validations when rules depend on state
For example, validate a field only if a user is an admin, or only on create
Use custom validators for anything more domain-specific
Things like checking date ranges, preventing invalid status transitions, or enforcing plan limits
Keep error messages clear
I also try to be practical about where validations belong:
NOT NULLThat matters because Rails uniqueness validation alone is not enough under concurrency. I’ll usually pair it with a unique index in the database.
A simple example would be:
name must be presentemail must be present and uniqueage must be a number greater than 18end_date is after start_dateIf the logic starts getting bulky, I’ll usually extract it into a custom validator or a form/service object instead of stuffing too much into the model. That keeps the model readable and makes the rule easier to test.
Knowing the questions is just the start. Work with experienced professionals who can help you perfect your answers, improve your presentation, and boost your confidence.
Comprehensive support to help you succeed at every stage of your interview journey
We've already delivered 1-on-1 mentorship to thousands of students, professionals, managers and executives. Even better, they've left an average rating of 4.9 out of 5 for our mentors.
Find Ruby on Rails Interview Coaches