-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
182: Interview Practice: System Design Basics for Ruby Backend Roles
When I first started interviewing for senior Ruby roles, I made a classic mistake. I focused entirely on the code—the elegant blocks, the metaprogramming, the "Ruby way"—and I completely ignored the infrastructure the code lived on. In a system design interview, the interviewer doesn't care if you know how to use Enumerable#reduce; they care if your application crashes when 10,000 users hit a single endpoint at once.
Take a look at this typical "first draft" of a CSV import feature. It looks clean, right?
class UserImportService
def initialize(file_path)
@file_path = file_path
end
def call
CSV.foreach(@file_path, headers: true) do |row|
User.create!(
email: row['email'],
first_name: row['first_name'],
last_name: row['last_name']
)
end
end
end
# In the Controller
def create
UserImportService.new(params[:file].path).call
render json: { message: "Import complete!" }, status: :ok
end
The Request-Response Timeout Trap
If you run this with a 10-row CSV on your laptop, it works perfectly. But in a real production environment, this is a ticking time bomb. Why? Because you're performing heavy I/O and database writes inside a synchronous HTTP request.
Most load balancers (like Nginx or AWS ALB) and app servers (like Puma) have a timeout—usually around 30 to 60 seconds. If that CSV has 50,000 rows, your Ruby process will be stuck in that foreach loop long after the load balancer has given up and returned a 504 Gateway Timeout to the user. Even worse, you're holding onto a database connection for the entire duration, which can lead to connection pool exhaustion, effectively taking down your entire site.
Offloading to Sidekiq and Batching Inserts
To fix this, we need to move from a synchronous design to an asynchronous one. We tell the user "We're working on it" immediately and move the heavy lifting to a background worker. I also want to replace the individual create! calls with a bulk insert to reduce the number of database round-trips.
# In the Controller
def create
# We upload the file to S3 first, then pass the URL to the worker
file_url = StorageService.upload(params[:file])
UserImportWorker.perform_async(file_url)
render json: { message: "Import started. You'll get an email when it's done!" }, status: :accepted
end
# The Worker
class UserImportWorker
include Sidekiq::Worker
def perform(file_url)
rows = []
CSV.foreach(download_file(file_url), headers: true) do |row|
rows << {
email: row['email'],
first_name: row['first_name'],
last_name: row['last_name'],
created_at: Time.current,
updated_at: Time.current
}
# Batch insert every 1000 rows to keep memory usage stable
if rows.size >= 1000
User.insert_all(rows)
rows.clear
end
end
User.insert_all(rows) unless rows.empty?
end
end
By using insert_all (introduced in Rails 6), we send one massive SQL query instead of 50,000 small ones. This is the difference between a task taking 20 minutes and taking 20 seconds.
Thinking in Layers: The System Design Mindset
When you're in a Ruby backend interview, you need to zoom out. Don't just talk about the Ruby class; talk about the layers. If the interviewer asks how to scale this system further, here is the mental checklist I use:
- The Database Layer: Are we hitting a bottleneck? I'd suggest adding a database index to the
emailcolumn to prevent slow lookups during duplicates checks. If the read volume is huge, I'd suggest adding Read Replicas so the primary DB only handles writes. - The Caching Layer: Do we have expensive queries running repeatedly? I'd implement Redis to cache frequently accessed data, reducing the load on PostgreSQL.
- The Application Layer: Is one Puma process not enough? We can scale horizontally by adding more app servers behind a Load Balancer.
- The Queue Layer: If
UserImportWorkeris slowing down other critical tasks (like password reset emails), I'd move it to a dedicated "low-priority" Sidekiq queue so it doesn't clog the system.
Remember, in these interviews, there is rarely one "correct" answer. The goal is to demonstrate that you understand the trade-offs. Choosing Redis adds complexity to your stack, but it buys you speed. Choosing insert_all skips ActiveRecord callbacks (like before_save), but it buys you performance. Mentioning those trade-offs is what signals to the interviewer that you're an experienced engineer.
📋 Practical Task
Design a Rate Limiter for a Ruby API
You are designing a public-facing API for a Ruby on Rails application. To prevent abuse and ensure fair usage, you need to implement a rate limiter that allows users a maximum of 100 requests per minute.
Your Task: Write a technical design proposal (in Markdown or plain text) that explains how you would implement this. Do not write the full implementation code, but provide a "pseudo-code" logic block. Your proposal must address the following:
- Storage: Where will you store the request counts? Explain why you chose this storage (e.g., Database vs. Redis) in terms of latency and performance.
- Logic: Describe the algorithm you'll use (e.g., Fixed Window, Sliding Window, or Token Bucket).
- Middleware: Where in the Ruby/Rails request lifecycle would this logic live to ensure it doesn't slow down the rest of the application?
- Response: What HTTP status code and header should be returned to the client when they exceed their limit?
There are no comments for now.