{
  "title": "Layered Rails Gems",
  "description": "Ruby and Rails gems recommended in Layered Design for Ruby on Rails Applications, Second Edition, organized by architectural layer.",
  "source_book": {
    "title": "Layered Design for Ruby on Rails Applications",
    "edition": "Second Edition",
    "author": "Vladimir Dementyev",
    "publisher": "Packt",
    "published": "2025-12-22"
  },
  "url": "https://whatagem.dev/",
  "generated": "2026-07-05",
  "counts": {
    "total": 70,
    "what_a_gem_callouts": 36,
    "inline_recommendations": 34,
    "layers": 13
  },
  "note": "Gem names, links, and page numbers are from the book. Descriptions and code examples are original. pdfPage is the book's printed page number. kind is 'What a gem' (formal callout) or 'Inline recommendation'.",
  "gems": [
    {
      "name": "Puma",
      "gem": "puma",
      "area": "HTTP and runtime",
      "kind": "Inline recommendation",
      "chapter": "Chapter 1",
      "pdfPage": 31,
      "source": "https://github.com/puma/puma",
      "description": "Multi-threaded, optionally clustered Rack web server and the Rails default; it accepts connections and turns raw HTTP into Rack requests your app can handle.",
      "example": "# config/puma.rb\nworkers ENV.fetch(\"WEB_CONCURRENCY\", 2)\nthreads 5, 5\n\n# Boot the app:\n# bundle exec puma -C config/puma.rb"
    },
    {
      "name": "Falcon",
      "gem": "falcon",
      "area": "HTTP and runtime",
      "kind": "Inline recommendation",
      "chapter": "Chapter 1",
      "pdfPage": 42,
      "source": "https://github.com/socketry/falcon",
      "description": "Fiber-based, multi-process web server that leverages Rails' fiber-readiness to handle concurrent I/O without dedicating a thread to every in-flight request.",
      "example": "# Gemfile\ngem \"falcon\"\n\n# Serve a Rack app on the async reactor:\n# bundle exec falcon serve --bind http://localhost:3000"
    },
    {
      "name": "Logidze",
      "gem": "logidze",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 1",
      "pdfPage": 48,
      "source": "https://github.com/palkan/logidze",
      "description": "Database-level change tracking with a Ruby API for audit trails and record time travel.",
      "example": "# migration\nadd_column :posts, :log_data, :jsonb\ncreate_logidze_trigger :posts\n\nclass Post < ApplicationRecord\n  has_logidze\nend"
    },
    {
      "name": "pg_trunk",
      "gem": "pg_trunk",
      "area": "Data and persistence",
      "kind": "Inline recommendation",
      "chapter": "Chapter 1",
      "pdfPage": 49,
      "source": "https://github.com/nepalez/pg_trunk",
      "description": "Rails migration helpers for PostgreSQL-specific objects such as domains, types, and functions.",
      "example": "class AddEmailDomain < ActiveRecord::Migration[8.0]\n  def change\n    create_domain :email_address, as: :text\n  end\nend"
    },
    {
      "name": "ROM",
      "gem": "rom",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 2",
      "pdfPage": 56,
      "source": "https://rom-rb.org",
      "description": "Persistence and mapping toolkit for Data Mapper-style Ruby applications.",
      "example": "# Gemfile\ngem \"rom\"\ngem \"rom-sql\"\n\nconfig = ROM::Configuration.new(:sql, ENV.fetch(\"DATABASE_URL\"))\ncontainer = ROM.container(config)"
    },
    {
      "name": "database_consistency",
      "gem": "database_consistency",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 2",
      "pdfPage": 61,
      "source": "https://github.com/djezzzl/database_consistency",
      "description": "Checks whether Rails validations and database constraints drift apart.",
      "example": "# Gemfile\ngem \"database_consistency\", group: :development\n\n# Run consistency checks.\nbundle exec database_consistency"
    },
    {
      "name": "database_validations",
      "gem": "database_validations",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 2",
      "pdfPage": 61,
      "source": "https://github.com/toptal/database_validations",
      "description": "Active Record validators that expect matching database constraints.",
      "example": "# Gemfile\ngem \"database_validations\"\n\nclass User < ApplicationRecord\n  validates :email, db_uniqueness: true\nend"
    },
    {
      "name": "Store Model",
      "gem": "store_model",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 2",
      "pdfPage": 68,
      "source": "https://github.com/DmitryTsepelev/store_model",
      "description": "Active Model objects backed by JSON columns or stores.",
      "example": "class Address\n  include StoreModel::Model\n  attribute :city, :string\nend\n\nclass User < ApplicationRecord\n  attribute :address, Address.to_type\nend"
    },
    {
      "name": "Frozen Record",
      "gem": "frozen_record",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 2",
      "pdfPage": 71,
      "source": "https://github.com/byroot/frozen_record",
      "description": "Read-only model layer over static YAML or JSON files.",
      "example": "class Country < FrozenRecord::Base\n  self.base_path = Rails.root.join(\"config/data\")\nend\n\nCountry.find(\"JO\")"
    },
    {
      "name": "Active Record Slotted Counters",
      "gem": "activerecord-slotted_counters",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 4",
      "pdfPage": 104,
      "source": "https://github.com/evilmartians/activerecord-slotted_counters",
      "description": "Reduces counter-cache lock contention by spreading counts across slots.",
      "example": "class Post < ApplicationRecord\n  has_many :comments\n  slotted_counter :comments_count\nend"
    },
    {
      "name": "Discard",
      "gem": "discard",
      "area": "Data and persistence",
      "kind": "Inline recommendation",
      "chapter": "Chapter 4",
      "pdfPage": 111,
      "source": "https://github.com/jhawthorn/discard",
      "description": "Lightweight soft deletion: it flips a discarded_at timestamp instead of destroying rows and adds kept/discarded scopes, hidden behind a concern in the book.",
      "example": "# add_column :posts, :discarded_at, :datetime\nclass Post < ApplicationRecord\n  include Discard::Model\nend\n\npost.discard   # sets discarded_at\nPost.kept       # rows still active"
    },
    {
      "name": "arel-helpers",
      "gem": "arel-helpers",
      "area": "Data and persistence",
      "kind": "What a gem",
      "chapter": "Chapter 6",
      "pdfPage": 163,
      "source": "https://github.com/camertron/arel-helpers",
      "description": "Arel extensions that reduce query-building boilerplate.",
      "example": "class Post < ApplicationRecord\n  include ArelHelpers::ArelTable\n  include ArelHelpers::JoinAssociation\nend\n\nPost.where(Post[:published].eq(true))"
    },
    {
      "name": "Sidekiq",
      "gem": "sidekiq",
      "area": "Jobs and scheduling",
      "kind": "What a gem",
      "chapter": "Chapter 1",
      "pdfPage": 43,
      "source": "https://github.com/mperham/sidekiq",
      "description": "Threaded background job processor backed by Redis.",
      "example": "# config/application.rb\nconfig.active_job.queue_adapter = :sidekiq\n\nclass SummaryJob < ApplicationJob\n  def perform(post_id) = Post.find(post_id).summarize!\nend"
    },
    {
      "name": "Solid Queue",
      "gem": "solid_queue",
      "area": "Jobs and scheduling",
      "kind": "What a gem",
      "chapter": "Chapter 1",
      "pdfPage": 45,
      "source": "https://github.com/rails/solid_queue",
      "description": "Database-backed Active Job adapter that keeps background processing inside Rails infrastructure.",
      "example": "# Gemfile\ngem \"solid_queue\"\n\n# config/application.rb\nconfig.active_job.queue_adapter = :solid_queue"
    },
    {
      "name": "GoodJob",
      "gem": "good_job",
      "area": "Jobs and scheduling",
      "kind": "What a gem",
      "chapter": "Chapter 3",
      "pdfPage": 77,
      "source": "https://github.com/bensheldon/good_job",
      "description": "PostgreSQL-backed Active Job processor with threads, scheduling, and a dashboard.",
      "example": "# Gemfile\ngem \"good_job\"\n\n# config/application.rb\nconfig.active_job.queue_adapter = :good_job"
    },
    {
      "name": "GlobalID",
      "gem": "globalid",
      "area": "Jobs and scheduling",
      "kind": "Inline recommendation",
      "chapter": "Chapter 3",
      "pdfPage": 81,
      "source": "https://github.com/rails/globalid",
      "description": "Represents any model as a URI (gid://app/User/1) so records can be enqueued for background jobs and reloaded from the database when the job runs.",
      "example": "gid = user.to_global_id.to_s   # \"gid://app/User/1\"\n\nGlobalID::Locator.locate(gid)   # => #<User id: 1>"
    },
    {
      "name": "active_job-performs",
      "gem": "active_job-performs",
      "area": "Jobs and scheduling",
      "kind": "Inline recommendation",
      "chapter": "Chapters 4 and 13",
      "pdfPage": 122,
      "source": "https://github.com/kaspth/active_job-performs",
      "description": "Adds a performs macro so models can enqueue convention-based jobs without hand-written job classes.",
      "example": "class Post < ApplicationRecord\n  performs :generate_summary\n\n  def generate_summary\n    summaries.create!(body: \"...\")\n  end\nend"
    },
    {
      "name": "Acidic Job",
      "gem": "acidic_job",
      "area": "Jobs and scheduling",
      "kind": "What a gem",
      "chapter": "Chapter 13",
      "pdfPage": 370,
      "source": "https://github.com/fractaledmind/acidic_job",
      "description": "Durable Active Job workflows with database-backed progress state.",
      "example": "class ImportCourseJob < ApplicationJob\n  include AcidicJob::Workflow\n\n  def perform(course_id)\n    with_acidity { step :download, course_id }\n  end\nend"
    },
    {
      "name": "Active Event Store",
      "gem": "active_event_store",
      "area": "Architecture and design",
      "kind": "What a gem",
      "chapter": "Chapter 4",
      "pdfPage": 106,
      "source": "https://github.com/palkan/active_event_store",
      "description": "Event-driven Rails conventions built on top of Rails Event Store.",
      "example": "class UserUpdated < ActiveEventStore::Event\n  attribute :user_id, :integer\nend\n\nActiveEventStore.event_store.publish(UserUpdated.new(user_id: user.id))"
    },
    {
      "name": "Downstream",
      "gem": "downstream",
      "area": "Architecture and design",
      "kind": "What a gem",
      "chapter": "Chapter 4",
      "pdfPage": 106,
      "source": "https://github.com/palkan/downstream",
      "description": "Rails-friendly event and listener abstractions for decoupling domain reactions.",
      "example": "class UserUpdatedEvent < Downstream::Event.define(:user)\nend\n\nDownstream.publish(UserUpdatedEvent.new(user: current_user))"
    },
    {
      "name": "active_record-associated_object",
      "gem": "active_record-associated_object",
      "area": "Architecture and design",
      "kind": "What a gem",
      "chapter": "Chapter 4",
      "pdfPage": 119,
      "source": "https://github.com/kaspth/active_record-associated_object",
      "description": "Adds a convention for model-owned collaborator objects through a has_object macro.",
      "example": "class User < ApplicationRecord\n  has_object :preferences\nend\n\nuser.preferences.time_zone"
    },
    {
      "name": "dry-effects",
      "gem": "dry-effects",
      "area": "Architecture and design",
      "kind": "What a gem",
      "chapter": "Chapter 4",
      "pdfPage": 125,
      "source": "https://dry-rb.org/gems/dry-effects",
      "description": "Algebraic effects for passing explicit context without Current-style global state.",
      "example": "include Dry::Effects.Reader(:current_user)\n\ndef call\n  AuditLog.create!(user: current_user)\nend"
    },
    {
      "name": "Interactor",
      "gem": "interactor",
      "area": "Architecture and design",
      "kind": "What a gem",
      "chapter": "Chapter 5",
      "pdfPage": 136,
      "source": "https://github.com/collectiveidea/interactor",
      "description": "Service object convention with success/failure result objects and organizers.",
      "example": "class PublishPost\n  include Interactor\n\n  def call\n    context.post.update!(published: true)\n  rescue ActiveRecord::RecordInvalid => error\n    context.fail!(error: error.message)\n  end\nend"
    },
    {
      "name": "dry-initializer",
      "gem": "dry-initializer",
      "area": "Architecture and design",
      "kind": "Inline recommendation",
      "chapter": "Chapters 5 and 12",
      "pdfPage": 137,
      "source": "https://dry-rb.org/gems/dry-initializer",
      "description": "Declarative initializer DSL for service objects and components.",
      "example": "class PublishPost\n  extend Dry::Initializer\n\n  param :post\n  option :notifier, default: proc { PostDelivery }\nend"
    },
    {
      "name": "dry-monads",
      "gem": "dry-monads",
      "area": "Architecture and design",
      "kind": "What a gem",
      "chapter": "Chapter 5",
      "pdfPage": 137,
      "source": "https://dry-rb.org/gems/dry-monads",
      "description": "Result, Maybe, and other monads for explicit service return values.",
      "example": "include Dry::Monads[:result]\n\ndef call(post)\n  post.update(published: true) ? Success(post) : Failure(post.errors)\nend"
    },
    {
      "name": "dry-container",
      "gem": "dry-container",
      "area": "Architecture and design",
      "kind": "Inline recommendation",
      "chapter": "Chapter 6",
      "pdfPage": 168,
      "source": "https://dry-rb.org/gems/dry-container",
      "description": "Simple dependency container for registering and resolving collaborators.",
      "example": "container = Dry::Container.new\ncontainer.register(\"repos.posts\") { PostRepository.new }\n\ncontainer.resolve(\"repos.posts\").find(1)"
    },
    {
      "name": "rails-pattern_matching",
      "gem": "rails-pattern_matching",
      "area": "State and workflow",
      "kind": "Inline recommendation",
      "chapter": "Chapter 7",
      "pdfPage": 178,
      "source": "https://github.com/kddnewton/rails-pattern_matching",
      "description": "Pattern matching support for Active Model and Active Record objects.",
      "example": "case post\nin { state: \"draft\", author: { admin?: true } }\n  post.publish!\nelse\n  false\nend"
    },
    {
      "name": "workflow",
      "gem": "workflow",
      "area": "State and workflow",
      "kind": "What a gem",
      "chapter": "Chapter 7",
      "pdfPage": 187,
      "source": "https://github.com/geekq/workflow",
      "description": "Finite-state machine DSL with transition diagrams and introspection APIs.",
      "example": "class Post\n  include Workflow\n\n  workflow do\n    state :draft do\n      event :publish, transitions_to: :published\n    end\n    state :published\n  end\nend"
    },
    {
      "name": "workflow-activerecord",
      "gem": "workflow-activerecord",
      "area": "State and workflow",
      "kind": "Inline recommendation",
      "chapter": "Chapter 7",
      "pdfPage": 187,
      "source": "https://github.com/geekq/workflow-activerecord",
      "description": "Active Record persistence integration for the workflow state-machine gem.",
      "example": "class Post < ApplicationRecord\n  include WorkflowActiverecord\n  workflow_column :state\nend"
    },
    {
      "name": "after_commit_everywhere",
      "gem": "after_commit_everywhere",
      "area": "Controllers and input",
      "kind": "Inline recommendation",
      "chapter": "Chapter 8",
      "pdfPage": 214,
      "source": "https://github.com/Envek/after_commit_everywhere",
      "description": "Transaction callbacks outside Active Record models.",
      "example": "class InviteUser\n  include AfterCommitEverywhere\n\n  def call\n    after_commit { UserMailer.invited(user).deliver_later }\n  end\nend"
    },
    {
      "name": "filterameter",
      "gem": "filterameter",
      "area": "Controllers and input",
      "kind": "Inline recommendation",
      "chapter": "Chapter 8",
      "pdfPage": 234,
      "source": "https://github.com/RockSolt/filterameter",
      "description": "Controller filtering DSL for readable parameter-to-scope pipelines.",
      "example": "class PostsController < ApplicationController\n  filterameter :index do\n    filter :status\n    filter :author_id\n  end\nend"
    },
    {
      "name": "has_scope",
      "gem": "has_scope",
      "area": "Controllers and input",
      "kind": "Inline recommendation",
      "chapter": "Chapter 8",
      "pdfPage": 234,
      "source": "https://github.com/heartcombo/has_scope",
      "description": "Maps controller parameters to Active Record scopes.",
      "example": "class PostsController < ApplicationController\n  has_scope :published, type: :boolean\n\n  def index\n    @posts = apply_scopes(Post).all\n  end\nend"
    },
    {
      "name": "Rubanok",
      "gem": "rubanok",
      "area": "Controllers and input",
      "kind": "What a gem",
      "chapter": "Chapter 8",
      "pdfPage": 236,
      "source": "https://github.com/palkan/rubanok",
      "description": "Parameter-based transformation pipelines for filters and other data flows.",
      "example": "class PostFilter < Rubanok::Processor\n  map :status do |status:|\n    raw.where(status: status)\n  end\nend\n\nPostFilter.call(Post.all, params)"
    },
    {
      "name": "Keynote",
      "gem": "keynote",
      "area": "Presentation and JSON",
      "kind": "What a gem",
      "chapter": "Chapter 9",
      "pdfPage": 251,
      "source": "https://github.com/evilmartians/keynote",
      "description": "Presenter objects with helper integration, test support, and presenter caching.",
      "example": "class PostPresenter < Keynote::Presenter\n  presents :post\n\n  def status_label = post.published? ? \"Published\" : \"Draft\"\nend"
    },
    {
      "name": "Jbuilder",
      "gem": "jbuilder",
      "area": "Presentation and JSON",
      "kind": "Inline recommendation",
      "chapter": "Chapter 9",
      "pdfPage": 256,
      "source": "https://github.com/rails/jbuilder",
      "description": "Template engine for building JSON in views, giving non-trivial API payloads the same partials and layout tooling that ERB gives HTML.",
      "example": "# app/views/posts/show.json.jbuilder\njson.title  @post.title\njson.author @post.author.name\njson.comments @post.comments, :id, :body"
    },
    {
      "name": "Alba",
      "gem": "alba",
      "area": "Presentation and JSON",
      "kind": "What a gem",
      "chapter": "Chapter 9",
      "pdfPage": 258,
      "source": "https://github.com/okuramasafumi/alba",
      "description": "Fast Ruby JSON serialization with a compact resource DSL.",
      "example": "class PostResource\n  include Alba::Resource\n  attributes :id, :title\n  attribute(:draft) { |post| post.draft? }\nend"
    },
    {
      "name": "Typelizer",
      "gem": "typelizer",
      "area": "Presentation and JSON",
      "kind": "What a gem",
      "chapter": "Chapter 9",
      "pdfPage": 260,
      "source": "https://github.com/skryukov/typelizer",
      "description": "Generates TypeScript definitions from Ruby serializers and models.",
      "example": "class PostSerializer\n  include Alba::Resource\n  include Typelizer::DSL\n\n  typelize id: :integer, title: :string\nend"
    },
    {
      "name": "ViewComponent",
      "gem": "view_component",
      "area": "Views and frontend",
      "kind": "What a gem",
      "chapter": "Chapter 12",
      "pdfPage": 331,
      "source": "https://viewcomponent.org",
      "description": "Ruby object plus template abstraction for reusable, testable Rails view components.",
      "example": "class BadgeComponent < ViewComponent::Base\n  def initialize(text:)\n    @text = text\n  end\nend\n\nrender BadgeComponent.new(text: \"Published\")"
    },
    {
      "name": "Papercraft",
      "gem": "papercraft",
      "area": "Views and frontend",
      "kind": "Inline recommendation",
      "chapter": "Chapter 12",
      "pdfPage": 336,
      "source": "https://github.com/digital-fabric/papercraft",
      "description": "Functional Ruby HTML templating alternative to ERB-style templates.",
      "example": "PostCard = Papercraft.html do |post:|\n  article do\n    h2 post.title\n  end\nend"
    },
    {
      "name": "Phlex",
      "gem": "phlex",
      "area": "Views and frontend",
      "kind": "Inline recommendation",
      "chapter": "Chapter 12",
      "pdfPage": 336,
      "source": "https://www.phlex.fun",
      "description": "Ruby classes that generate HTML without separate template files.",
      "example": "class Title < Phlex::HTML\n  def view_template\n    h1 { \"Layered Rails\" }\n  end\nend"
    },
    {
      "name": "Lookbook",
      "gem": "lookbook",
      "area": "Views and frontend",
      "kind": "Inline recommendation",
      "chapter": "Chapter 14",
      "pdfPage": 395,
      "source": "https://github.com/ViewComponent/lookbook",
      "description": "Interactive preview UI and workbench for ViewComponents, toggled on in development and browsable at /lookbook.",
      "example": "# Gemfile\ngem \"lookbook\", group: :development\n\nclass BadgeComponentPreview < ViewComponent::Preview\n  def default\n    render BadgeComponent.new(text: \"Published\")\n  end\nend\n# Browse previews at /lookbook"
    },
    {
      "name": "Action Policy",
      "gem": "action_policy",
      "area": "Authorization and notification",
      "kind": "What a gem",
      "chapter": "Chapter 10",
      "pdfPage": 280,
      "source": "https://github.com/palkan/action_policy",
      "description": "Authorization framework centered on policy objects, performance, and testing support.",
      "example": "class PostPolicy < ApplicationPolicy\n  def update?\n    user.admin? || record.author_id == user.id\n  end\nend\n\nauthorize! @post, to: :update?"
    },
    {
      "name": "action_native_push",
      "gem": "action_native_push",
      "area": "Authorization and notification",
      "kind": "Inline recommendation",
      "chapter": "Chapter 11",
      "pdfPage": 297,
      "source": "https://github.com/basecamp/action_native_push",
      "description": "Basecamp's toolkit for sending native mobile push notifications from Rails to registered device tokens, slotting in alongside a notifications layer.",
      "example": "# Gemfile\ngem \"action_native_push\"\n\n# Push to a registered device token:\nActionNativePush.push(to: device_token,\n  title: \"New post\", body: post.title)"
    },
    {
      "name": "Active Delivery",
      "gem": "active_delivery",
      "area": "Authorization and notification",
      "kind": "What a gem",
      "chapter": "Chapter 11",
      "pdfPage": 304,
      "source": "https://github.com/palkan/active_delivery",
      "description": "Notification layer with delivery objects and Action Mailer-like APIs.",
      "example": "class PostDelivery < ApplicationDelivery\n  mailer :post_mailer\n\n  def published(post)\n    mail(to: params[:user].email, post: post)\n  end\nend"
    },
    {
      "name": "Noticed",
      "gem": "noticed",
      "area": "Authorization and notification",
      "kind": "What a gem",
      "chapter": "Chapter 11",
      "pdfPage": 309,
      "source": "https://github.com/excid3/noticed",
      "description": "Notification objects that define delivery channels and per-channel payloads.",
      "example": "class PostPublishedNotification < Noticed::Event\n  deliver_by :email, mailer: \"PostMailer\", method: :published\n  deliver_by :database\nend\n\nPostPublishedNotification.with(post: post).deliver_later(user)"
    },
    {
      "name": "ImageProcessing",
      "gem": "image_processing",
      "area": "Media and realtime",
      "kind": "Inline recommendation",
      "chapter": "Chapter 3",
      "pdfPage": 86,
      "source": "https://github.com/janko/image_processing",
      "description": "Uniform Ruby API over libvips and ImageMagick that powers Active Storage variants for resizing, cropping, and format conversion.",
      "example": "# Gemfile\ngem \"image_processing\", \"~> 1.13\"\n\n# Active Storage variant (libvips or ImageMagick):\nuser.avatar.variant(resize_to_limit: [300, 300]).processed"
    },
    {
      "name": "imgproxy-rails",
      "gem": "imgproxy-rails",
      "area": "Media and realtime",
      "kind": "Inline recommendation",
      "chapter": "Chapter 15",
      "pdfPage": 428,
      "source": "https://github.com/imgproxy/imgproxy-rails",
      "description": "Integrates imgproxy with Active Storage variants so image transformation can move out of Rails.",
      "example": "# Gemfile\ngem \"imgproxy-rails\"\n\nimage_tag user.avatar.variant(resize_to_limit: [300, 300])"
    },
    {
      "name": "Ruby OpenAI",
      "gem": "ruby-openai",
      "area": "AI and agents",
      "kind": "What a gem",
      "chapter": "Chapter 13",
      "pdfPage": 341,
      "source": "https://github.com/alexrudall/ruby-openai",
      "description": "OpenAI-compatible Ruby client for chat, embeddings, streaming, and related LLM APIs.",
      "example": "client = OpenAI::Client.new(access_token: ENV.fetch(\"OPENAI_API_KEY\"))\nresponse = client.chat(parameters: {\n  model: \"gpt-4o-mini\",\n  messages: [{role: \"user\", content: \"Summarize this post\"}]\n})"
    },
    {
      "name": "Active Agent",
      "gem": "activeagent",
      "area": "AI and agents",
      "kind": "What a gem",
      "chapter": "Chapter 13",
      "pdfPage": 347,
      "source": "https://github.com/activeagents/activeagent",
      "description": "Rails-oriented agent abstraction for organizing prompts, generations, and LLM integrations.",
      "example": "class SummaryAgent < ApplicationAgent\n  def summarize(post)\n    prompt(message: \"Summarize: #{post.body}\")\n  end\nend"
    },
    {
      "name": "ruby_llm-schema",
      "gem": "ruby_llm-schema",
      "area": "AI and agents",
      "kind": "Inline recommendation",
      "chapter": "Chapter 13",
      "pdfPage": 361,
      "source": "https://github.com/danielfriis/ruby_llm-schema",
      "description": "Ruby DSL for building JSON Schemas used in structured LLM responses and tools.",
      "example": "schema = RubyLLM::Schema.create do\n  string :content, description: \"Summary\"\n  array :keywords, of: :string\nend\n\nschema.to_json_schema"
    },
    {
      "name": "RubyLLM",
      "gem": "ruby_llm",
      "area": "AI and agents",
      "kind": "What a gem",
      "chapter": "Chapter 13",
      "pdfPage": 361,
      "source": "https://rubyllm.com",
      "description": "Unified Ruby interface and ecosystem for major LLM providers.",
      "example": "chat = RubyLLM.chat(model: \"gpt-4o-mini\")\nchat.ask(\"Summarize this in one sentence.\")"
    },
    {
      "name": "prompt_engine",
      "gem": "prompt_engine",
      "area": "AI and agents",
      "kind": "Inline recommendation",
      "chapter": "Chapter 13",
      "pdfPage": 375,
      "source": "https://github.com/aviflombaum/prompt_engine",
      "description": "Rails engine for database-backed prompt storage and an admin UI.",
      "example": "# config/routes.rb\nmount PromptEngine::Engine => \"/prompts\"\n\nprompt = PromptEngine::Prompt.find_by!(key: \"summary\")\nprompt.render(post: post)"
    },
    {
      "name": "baran",
      "gem": "baran",
      "area": "AI and agents",
      "kind": "Inline recommendation",
      "chapter": "Chapter 13",
      "pdfPage": 383,
      "source": "https://github.com/moeki0/baran",
      "description": "Text splitter for chunking large documents before embedding or retrieval.",
      "example": "splitter = Baran::Splitter.new(chunk_size: 800)\nchunks = splitter.call(course.transcript)\n\nchunks.each { |chunk| DocumentChunk.create!(body: chunk) }"
    },
    {
      "name": "Neighbor",
      "gem": "neighbor",
      "area": "AI and agents",
      "kind": "Inline recommendation",
      "chapter": "Chapter 13",
      "pdfPage": 384,
      "source": "https://github.com/ankane/neighbor",
      "description": "Nearest-neighbor vector search for Rails models, commonly used for embeddings.",
      "example": "class DocumentChunk < ApplicationRecord\n  has_neighbors :embedding\nend\n\nDocumentChunk.nearest_neighbors(:embedding, query_embedding, distance: \"cosine\").limit(5)"
    },
    {
      "name": "Fast MCP",
      "gem": "fast-mcp",
      "area": "AI and agents",
      "kind": "What a gem",
      "chapter": "Chapter 13",
      "pdfPage": 386,
      "source": "https://github.com/yjacquin/fast-mcp",
      "description": "Ruby toolkit for defining MCP tools, resources, and transports.",
      "example": "class CreateTagTool < FastMcp::Tool\n  description \"Create a tag\"\n\n  def call(name:)\n    Tag.create!(name: name)\n  end\nend"
    },
    {
      "name": "dotenv",
      "gem": "dotenv",
      "area": "Configuration and infrastructure",
      "kind": "Inline recommendation",
      "chapter": "Chapter 14",
      "pdfPage": 394,
      "source": "https://github.com/bkeepers/dotenv",
      "description": "Loads environment variables from .env files for local development.",
      "example": "# Gemfile\ngem \"dotenv-rails\", groups: [:development, :test]\n\n# .env\nOPENAI_API_KEY=sk-local"
    },
    {
      "name": "Anyway Config",
      "gem": "anyway_config",
      "area": "Configuration and infrastructure",
      "kind": "What a gem",
      "chapter": "Chapter 14",
      "pdfPage": 404,
      "source": "https://github.com/palkan/anyway_config",
      "description": "Typed, named configuration objects that can read from files, env vars, and other sources.",
      "example": "class GithubConfig < Anyway::Config\n  attr_config :token, :repo\nend\n\nGithubConfig.new.token"
    },
    {
      "name": "Yabeda",
      "gem": "yabeda",
      "area": "Configuration and infrastructure",
      "kind": "What a gem",
      "chapter": "Chapter 15",
      "pdfPage": 422,
      "source": "https://github.com/yabeda-rb/yabeda",
      "description": "Instrumentation framework with a standard metrics API and exporter adapters.",
      "example": "Yabeda.configure do\n  counter :github_api_calls_total\nend\n\nYabeda.github_api_calls_total.increment"
    },
    {
      "name": "trace_location",
      "gem": "trace_location",
      "area": "Testing and quality",
      "kind": "What a gem",
      "chapter": "Chapter 1",
      "pdfPage": 36,
      "source": "https://github.com/yhirano55/trace_location",
      "description": "Runtime tracing helper for seeing which Ruby methods are touched during a request or operation.",
      "example": "require \"trace_location\"\n\nrequest = Rack::MockRequest.env_for(\"http://example.test\")\nTraceLocation.trace(format: :log) do\n  Rails.application.call(request)\nend"
    },
    {
      "name": "gvl-tracing",
      "gem": "gvl-tracing",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 1",
      "pdfPage": 42,
      "source": "https://github.com/ivoanjo/gvl-tracing",
      "description": "Produces a timeline of Global VM Lock usage that can be opened in Perfetto.",
      "example": "require \"gvl-tracing\"\n\nGvlTracing.start(\"tmp/gvl.json\")\nThread.new { 5_000_000.times { 1 + 1 } }.join\nGvlTracing.stop"
    },
    {
      "name": "gvltools",
      "gem": "gvltools",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 1",
      "pdfPage": 42,
      "source": "https://github.com/Shopify/gvltools",
      "description": "Instrumentation helpers for measuring Ruby Global VM Lock behavior on threaded workloads.",
      "example": "# Gemfile\ngem \"gvltools\", group: :development\n\n# Wrap a representative workload and inspect the reported GVL timing.\nbundle exec ruby script/profile_gvl.rb"
    },
    {
      "name": "benchmark-ips",
      "gem": "benchmark-ips",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 2",
      "pdfPage": 69,
      "source": "https://github.com/evanphx/benchmark-ips",
      "description": "Iterations-per-second benchmarking for comparing Ruby implementations.",
      "example": "require \"benchmark/ips\"\n\nBenchmark.ips do |x|\n  x.report(\"scope\") { Post.published.to_a }\n  x.compare!\nend"
    },
    {
      "name": "benchmark-memory",
      "gem": "benchmark-memory",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 2",
      "pdfPage": 70,
      "source": "https://github.com/michaelherold/benchmark-memory",
      "description": "Memory allocation benchmarking for Ruby code paths.",
      "example": "require \"benchmark/memory\"\n\nBenchmark.memory do |x|\n  x.report(\"serializer\") { PostSerializer.new(Post.first).as_json }\n  x.compare!\nend"
    },
    {
      "name": "Attractor",
      "gem": "attractor",
      "area": "Testing and quality",
      "kind": "What a gem",
      "chapter": "Chapter 2",
      "pdfPage": 72,
      "source": "https://github.com/julianrubisch/attractor",
      "description": "Visualizes churn and complexity so large refactoring targets become obvious.",
      "example": "# Gemfile\ngem \"attractor\", group: :development\n\nbundle exec attractor report"
    },
    {
      "name": "Flog",
      "gem": "flog",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 2",
      "pdfPage": 72,
      "source": "https://github.com/seattlerb/flog",
      "description": "Ruby code complexity reporter used to spot hard-to-maintain classes.",
      "example": "# Gemfile\ngem \"flog\", group: :development\n\nbundle exec flog app/models app/services"
    },
    {
      "name": "callback_hell",
      "gem": "callback_hell",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 4",
      "pdfPage": 101,
      "source": "https://github.com/evilmartians/callback_hell",
      "description": "Audits Rails models for callback-heavy designs.",
      "example": "# Gemfile\ngem \"callback_hell\", group: :development\n\n# Show callback counts by model.\nbundle exec rake ch:callbacks"
    },
    {
      "name": "with_model",
      "gem": "with_model",
      "area": "Testing and quality",
      "kind": "What a gem",
      "chapter": "Chapter 4",
      "pdfPage": 113,
      "source": "https://github.com/Casecommons/with_model",
      "description": "Builds throwaway Active Record models and tables inside tests.",
      "example": "with_model :Widget do\n  table { |t| t.string :name }\n  model { validates :name, presence: true }\nend\n\nexpect(Widget.new).not_to be_valid"
    },
    {
      "name": "erb_lint",
      "gem": "erb_lint",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 12",
      "pdfPage": 324,
      "source": "https://github.com/Shopify/erb-lint",
      "description": "Linter for ERB templates, including rules for view partial hygiene.",
      "example": "# Gemfile\ngem \"erb_lint\", group: :development\n\n# Lint ERB templates.\nbundle exec erb_lint app/views"
    },
    {
      "name": "Capybara",
      "gem": "capybara",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 12",
      "pdfPage": 333,
      "source": "https://github.com/teamcapybara/capybara",
      "description": "High-level DSL for querying and asserting rendered HTML; used in the book to test ViewComponent output and to drive integration and system tests.",
      "example": "# In a component or system test:\nrender_inline(BadgeComponent.new(text: \"Published\"))\nassert_selector \"span.badge\", text: \"Published\""
    },
    {
      "name": "WebMock",
      "gem": "webmock",
      "area": "Testing and quality",
      "kind": "Inline recommendation",
      "chapter": "Chapter 14",
      "pdfPage": 398,
      "source": "https://github.com/bblimke/webmock",
      "description": "Stubs and asserts outbound HTTP requests in tests.",
      "example": "stub_request(:get, \"https://api.example.test/users/1\")\n  .to_return(status: 200, body: {name: \"Ada\"}.to_json)\n\nNet::HTTP.get(URI(\"https://api.example.test/users/1\"))"
    }
  ]
}
