Gems, Bundler and project layout
Gemfile and lockfile discipline, gem version constraints, gemspec versus Gemfile, Rake tasks, load paths and a layout that scales.
Gemfile and the lockfile
# Gemfile
source "https://rubygems.org"
ruby "3.3.4"
gem "rack", "~> 3.1" # >= 3.1, < 4.0
gem "puma", "~> 6.4"
gem "pg", ">= 1.5", "< 2.0"
gem "json"
group :development, :test do
gem "rspec", "~> 3.13"
gem "rubocop", require: false
end
group :production do
gem "newrelic_rpm"
end
# a gem from a git branch
gem "acme-client", git: "https://github.com/acme/client.git", branch: "main"bundle install # resolves and writes Gemfile.lock
bundle update rack # move one gem within its constraint
bundle exec rspec # run against locked versions
bundle outdated
bundle lock --add-platform x86_64-linux arm64-darwin- Commit
Gemfile.lockfor applications, gitignore it for gems and libraries. bundle execactivates exactly the locked versions; runningrspecdirectly can load a newer system gem and give a false result.- The pessimistic operator
~> 3.1means>= 3.1, < 4.0, while~> 3.1.0means>= 3.1.0, < 3.2.0. The extra digit tightens it a lot. require: falsekeeps a tool out of the boot path when you only invoke it from the command line.
💡
A gem installed globally but missing from the Gemfile works on your machine and fails in CI. If code compiles locally but not in the pipeline, that discrepancy is the first thing to check.
Load paths and project layout
# a conventional layout
# myapp/
# bin/report executable
# lib/myapp.rb requires everything under lib/myapp/
# lib/myapp/client.rb class Myapp::Client
# spec/ tests mirror lib/
# Rakefile
# myapp.gemspec
# Gemfile
# lib/myapp.rb
require_relative "myapp/version"
require_relative "myapp/client"
module Myapp
class Error < StandardError; end
end
# bin/report
#!/usr/bin/env ruby
require "bundler/setup"
require "myapp"
puts Myapp::Client.new(ENV.fetch("API_URL")).summary
# Rakefile
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
task default: :spec
desc "Import a CSV of books"
task :import, [:path] do |_t, args|
Myapp::Import.new(args[:path]).call
end| Dependency | Declared in | Purpose |
|---|---|---|
| Runtime library of the gem | gemspec add_dependency | Installed whenever the gem is |
| Development tool | gemspec add_development_dependency | Only in the gem's own project |
| Application dependency | Gemfile | Not published to consumers |
| Ruby version | ruby "3.3.4" in Gemfile or gemspec | Fails fast on the wrong interpreter |
Use require_relative for files inside your own project and require for gems. Mixing them up makes a library work from one entry point and break from another, because the load path differs.
FAQ
Should I run bundle update regularly?
Yes, in small batches on a branch with the test suite as the gate. One giant update after a year makes it impossible to identify which of forty version bumps broke a test.
How do I debug a LoadError?
Check that the gem is in the Gemfile and installed (
bundle list), that you are running through bundle exec, and that the require path matches the gem's file - some gems have a different require name than their gem name.Related
Packaging and distributing gems and CLIs Testing with Minitest and RSpec
Last refreshed 2026-09-18.