<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wilbur Suero</title>
    <description>The latest articles on DEV Community by Wilbur Suero (@travelingwilbur).</description>
    <link>https://dev.to/travelingwilbur</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F38352%2Fb3f7db58-dbe2-42d2-a48e-fbaa9768b36e.jpg</url>
      <title>DEV Community: Wilbur Suero</title>
      <link>https://dev.to/travelingwilbur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/travelingwilbur"/>
    <language>en</language>
    <item>
      <title>AISpec: RSpec for AI Behavior, Not AI Outputs</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:39:25 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/aispec-rspec-for-ai-behavior-not-ai-outputs-1dia</link>
      <guid>https://dev.to/travelingwilbur/aispec-rspec-for-ai-behavior-not-ai-outputs-1dia</guid>
      <description>&lt;p&gt;Standard RSpec assertions expect exact values: &lt;code&gt;expect(user.name).to eq("Alice")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;LLMs break that contract immediately. Run a prompt three times at temperature 0.2, and you get three different phrasing variations.&lt;/p&gt;

&lt;p&gt;When teams add AI features to Rails apps, they usually fall into one of two patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing brittle regex assertions that shatter whenever the model picks a synonym or shifts sentence structure.&lt;/li&gt;
&lt;li&gt;Offloading evals to a third-party SaaS dashboard that runs weekly batch jobs—completely disconnected from local &lt;code&gt;bundle exec rspec&lt;/code&gt; runs and CI pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither scale for production. Forget testing word-for-word string matches. You need to enforce what the AI pipeline is permitted, required, or blocked from doing.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://github.com/wilburhimself/aispec" rel="noopener noreferrer"&gt;AISpec&lt;/a&gt;&lt;/strong&gt; to solve this for Ruby and Rails. It's an open-source gem that brings invariant behavioral contracts, statistical confidence bounds, and native RSpec assertions into your test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Behavioral invariants vs. static text assertions
&lt;/h2&gt;

&lt;p&gt;Traditional code lives in a deterministic space. LLMs operate probabilistically, but your product constraints are still binary.&lt;/p&gt;

&lt;p&gt;An LLM feature needs to satisfy two types of bounds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────────────────────────┐
                    │     LLM Application Constraints     │
                    └──────────────────┬──────────────────┘
                                       │
            ┌──────────────────────────┴──────────────────────────┐
            ▼                                                     ▼
┌─────────────────────────┐                           ┌─────────────────────────┐
│  Deterministic Bounds   │                           │   Probabilistic Bounds  │
├─────────────────────────┤                           ├─────────────────────────┤
│ • Valid JSON Schema     │                           │ • Semantic Relevance    │
│ • Strict Latency Budget │                           │ • Empathetic Tone       │
│ • Max Cost ($/call)     │                           │ • Factual Grounding     │
│ • Tool Calling Sequence │                           │ • LLM-as-a-Judge Score  │
│ • System Prompt Privacy │                           └─────────────────────────┘
└─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deterministic bounds are pass/fail. The response either matches your JSON schema or it doesn't. Execution finished under 2,500ms, or it blew past your budget. The app called &lt;code&gt;fetch_account_status&lt;/code&gt; before &lt;code&gt;process_refund&lt;/code&gt;, or it failed the sequence.&lt;/p&gt;

&lt;p&gt;Probabilistic bounds grade response quality. An LLM judge evaluates whether the output stays factual against provided context or hits the expected tone threshold.&lt;/p&gt;

&lt;p&gt;AISpec unifies both under a single contract file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defining behavioral contracts in AISpec
&lt;/h2&gt;

&lt;p&gt;Contracts live in YAML files alongside your codebase. You declare the provider, model, input dataset, and invariant rules.&lt;/p&gt;

&lt;p&gt;Here is a contract for a customer support bot (&lt;code&gt;contracts/support.yml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-bot&lt;/span&gt;

&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;

&lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;datasets/support_cases.yml&lt;/span&gt;

&lt;span class="na"&gt;contracts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Structural &amp;amp; Formatting Constraints&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
    &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticket_id"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;citation_format&lt;/span&gt;

  &lt;span class="c1"&gt;# Operational Constraints&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;max_latency&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2500&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cost&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.03&lt;/span&gt;

  &lt;span class="c1"&gt;# Safety &amp;amp; Tooling Constraints&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;must_not_reveal_system_prompt&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tool_order&lt;/span&gt;
    &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch_account_status"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process_refund"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Probabilistic Quality Criteria (LLM-as-a-Judge)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;judge&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;The response must be polite, accurate to our refund policy, and concise.&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use inline shorthand rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;contracts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;must_return_valid_json&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;latency &amp;lt; 2500ms&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cost &amp;lt; $0.03&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;confidence &amp;gt;= &lt;/span&gt;&lt;span class="m"&gt;0.85&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;aispec run contracts/support.yml&lt;/code&gt; from your terminal to execute the dataset and check every rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aispec run contracts/support.yml

Running 12 contracts &lt;span class="k"&gt;for &lt;/span&gt;support-bot &lt;span class="o"&gt;(&lt;/span&gt;openai / gpt-4o&lt;span class="o"&gt;)&lt;/span&gt;...

Case &lt;span class="c"&gt;#1 (Input: "How do I request a refund?")&lt;/span&gt;
  PASS  json: Output is valid JSON matching target schema
  PASS  citation_format: Response contains valid citation formatting
  PASS  max_latency: Latency 142.0ms &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; 2500ms
  PASS  cost: Cost &lt;span class="nv"&gt;$0&lt;/span&gt;.0018 &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;.0300
  PASS  must_not_reveal_system_prompt: System prompt protected
  PASS  tool_order: Tool sequence &lt;span class="o"&gt;[&lt;/span&gt;fetch_account_status, process_refund] matched
  PASS  judge: Score 0.94 &lt;span class="o"&gt;(&lt;/span&gt;threshold 0.85&lt;span class="o"&gt;)&lt;/span&gt;: Response accurate and empathetic

Overall
  Passed: 12/12 &lt;span class="o"&gt;(&lt;/span&gt;100.0%&lt;span class="o"&gt;)&lt;/span&gt;
  Confidence Interval: 95.0% CI &lt;span class="o"&gt;[&lt;/span&gt;75.8%, 100.0%]
  Mean Latency: 142.0ms
  Total Cost: &lt;span class="nv"&gt;$0&lt;/span&gt;.0036
  Variance: Low
  Recommendation: Stable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AISpec computes a Wilson score 95% confidence interval and tracks variance across runs. If small sample sizes or wild output swings threaten reliability, AISpec flags the instability before bad prompt edits land in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmarking across models
&lt;/h2&gt;

&lt;p&gt;Switching from &lt;code&gt;gpt-4o&lt;/code&gt; to &lt;code&gt;claude-3-5-sonnet&lt;/code&gt; or a local &lt;code&gt;llama3&lt;/code&gt; instance usually feels like guesswork. You swap model names, run a few manual prompts, and hope nothing broke.&lt;/p&gt;

&lt;p&gt;AISpec lets you benchmark candidate models against your exact contract suite in one terminal command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aispec compare gpt-4o claude-3-5-sonnet llama3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Success Rate&lt;/th&gt;
&lt;th&gt;95% Confidence CI&lt;/th&gt;
&lt;th&gt;Cost / Run&lt;/th&gt;
&lt;th&gt;Mean Latency&lt;/th&gt;
&lt;th&gt;Variance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gpt-4o&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[92.1%, 99.7%]&lt;/td&gt;
&lt;td&gt;$0.0024&lt;/td&gt;
&lt;td&gt;1.1s&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;claude-3-5-sonnet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97.5%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[91.0%, 99.3%]&lt;/td&gt;
&lt;td&gt;$0.0042&lt;/td&gt;
&lt;td&gt;1.4s&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;llama3 (Ollama local)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[82.4%, 95.6%]&lt;/td&gt;
&lt;td&gt;$0.0000&lt;/td&gt;
&lt;td&gt;0.8s&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This shifts model selection from subjective vibe checks to clear metrics. You instantly see whether a cheaper or open-weights model meets your latency and safety budgets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rails integration
&lt;/h2&gt;

&lt;p&gt;AI evals shouldn't require a separate Python toolchain or external dashboard. In a Rails application, your behavioral contracts belong right inside &lt;code&gt;spec/contracts/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Initializer and custom assertions
&lt;/h3&gt;

&lt;p&gt;Set default providers and register custom domain rules in &lt;code&gt;config/initializers/aispec.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/aispec.rb&lt;/span&gt;
&lt;span class="no"&gt;AISpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"AISPEC_PROVIDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"openai"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_model&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gpt-4o"&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;judge_provider&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"openai"&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;judge_model&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gpt-4o"&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Register domain-specific assertions using the AISpec plugin DSL&lt;/span&gt;
&lt;span class="no"&gt;AISpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plugin&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;assertion&lt;/span&gt; &lt;span class="ss"&gt;:valid_tenant_isolation&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:tenant_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;contains_tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:output&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Tenant: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;contains_tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;contains_tenant&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Output correctly bound to Tenant &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Security Violation: Missing Tenant &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Running contracts in RSpec
&lt;/h3&gt;

&lt;p&gt;Run your YAML contracts directly inside standard RSpec tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/contracts/support_contract_spec.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"rails_helper"&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"Support Bot Behavioral Contract"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :contract&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"satisfies safety, format, and cost invariant budgets"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;contract_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"spec/contracts/support.yml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;AISpec&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Core&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contract_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:statistics&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;

    &lt;span class="c1"&gt;# Enforce zero assertion failures&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:failed_assertions&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:runs&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;flat_map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:assertion_results&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:passed?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="s2"&gt;"Contract Violations:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Enforce statistical quality bounds&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:success_rate&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;95.0&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:mean_latency&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;2500.0&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:total_cost&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Continuous verification in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Because AISpec outputs JSON, JUnit XML, and Markdown tables, you can run contract checks on every pull request using GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/ai_contracts.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AI Behavioral Verification&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;verify-contracts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.3'&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Execute AISpec Contracts&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;bundle exec aispec run contracts/support.yml --format markdown --output pr_comment.md&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post Behavioral Report to Pull Request&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;marooned/actions-comment-pull-request@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;filePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pr_comment.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When someone modifies a prompt in a pull request, CI comments with a table showing whether latency jumped, costs rose, or score bounds failed.&lt;/p&gt;




&lt;p&gt;AI tooling makes writing code easy, but maintaining system reliability remains the real work.&lt;/p&gt;

&lt;p&gt;Without explicit behavioral boundaries, every prompt change introduces risk. Defining invariant rules—schema checks, latency budgets, tool sequences, and statistical thresholds—turns unpredictable model outputs into reliable system components.&lt;/p&gt;

&lt;p&gt;Check out &lt;strong&gt;&lt;a href="https://github.com/wilburhimself/aispec" rel="noopener noreferrer"&gt;AISpec on GitHub&lt;/a&gt;&lt;/strong&gt; or run &lt;code&gt;gem install aispec&lt;/code&gt; to add behavioral specs to your Rails application.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>ai</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>The Two Sources of Noise Every LLM Evaluation System Must Handle</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Wed, 22 Jul 2026 18:12:14 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/the-two-sources-of-noise-every-llm-evaluation-system-must-handle-55l4</link>
      <guid>https://dev.to/travelingwilbur/the-two-sources-of-noise-every-llm-evaluation-system-must-handle-55l4</guid>
      <description>&lt;p&gt;&lt;em&gt;Traditional tests are binary. LLM evaluations are a forecast.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Every software engineer has experienced the satisfaction of a green test suite.&lt;/p&gt;

&lt;p&gt;You change the code.&lt;/p&gt;

&lt;p&gt;You run the tests.&lt;/p&gt;

&lt;p&gt;Everything passes.&lt;/p&gt;

&lt;p&gt;That simple feedback loop has shaped how we've built software for decades.&lt;/p&gt;

&lt;p&gt;Then LLMs quietly broke one of software engineering's most trusted feedback loops.&lt;/p&gt;

&lt;p&gt;You run the exact same evaluation twice...&lt;/p&gt;

&lt;p&gt;...and get two different answers.&lt;/p&gt;

&lt;p&gt;Nothing changed.&lt;/p&gt;

&lt;p&gt;At least, nothing you can see.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fallacy of the Scoreboard
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;An evaluation score looks like a measurement.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;It's an estimate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't entirely new. Performance benchmarks, A/B tests, and distributed systems have always required statistical thinking. LLM evaluation simply makes that reality impossible to ignore.&lt;/p&gt;

&lt;p&gt;When we built our internal LLM evaluation framework, we expected to run our benchmarks, get a single score (say, 84%), tweak a prompt, and see that score change. If it dropped to 81%, we assumed a regression. If it rose to 87%, we assumed an improvement.&lt;/p&gt;

&lt;p&gt;We spent weeks chasing ghosts, tweaking prompts to fix a drop in score, only to realize the next run would swing back.&lt;/p&gt;

&lt;p&gt;The mistake isn't that LLM evaluations are noisy.&lt;/p&gt;

&lt;p&gt;The mistake is expecting them not to be.&lt;/p&gt;

&lt;p&gt;Once you stop thinking of evaluation as testing—and start thinking of it as measurement—the architecture almost designs itself.&lt;/p&gt;

&lt;p&gt;To build an AI evaluation pipeline that actually guides production decisions, you must realize:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not all evaluation noise is the same.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some fluctuations come from the conversations we are evaluating. Others come from the evaluator itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhvff8xs29b7897jwcpl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhvff8xs29b7897jwcpl.png" alt="Evaluation Noise" width="533" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every score fluctuation is a question before it's an answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Did the model change?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Did the benchmark change?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Or did the measurement change?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treating all three as "the score changed" is how evaluation systems lose credibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #1: Sometimes There Isn't a Right Answer
&lt;/h2&gt;

&lt;p&gt;Imagine a user who replies to your LLM assistant with a single word:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Stop"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What should the assistant do?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An SMS opt-out request:&lt;/strong&gt; "You have been unsubscribed."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A conversation pause:&lt;/strong&gt; "Let's stop here for today. Let me know when you want to continue."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frustration:&lt;/strong&gt; "I'm sorry, let's try another approach."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A change of topic:&lt;/strong&gt; "Okay, let's stop talking about this and try something else."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without additional context, all of these interpretations are reasonable. Even human reviewers will disagree on the best response. The ambiguity exists in the conversation itself. No prompt engineering or model tuning can eliminate it.&lt;/p&gt;

&lt;p&gt;Statisticians call this &lt;strong&gt;aleatoric uncertainty&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The terminology isn't important. The engineering implication is.&lt;/p&gt;

&lt;p&gt;If the uncertainty lives in the conversation, improving the judge won't fix it. You're optimizing the wrong part of the system. You cannot debug or code your way out of it because the input itself lacks the information required to produce a single deterministic answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Mitigate Task Ambiguity
&lt;/h3&gt;

&lt;p&gt;In practice, reducing task ambiguity usually comes down to four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Richer conversational context:&lt;/strong&gt; Ensure your fixtures contain multi-turn history. Context resolves ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-derived fixtures:&lt;/strong&gt; Build your benchmark using anonymized, cleaned production logs that represent real-world usage rather than synthetic test cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit ambiguity labels:&lt;/strong&gt; Tag fixtures that have multiple valid responses so they aren't judged on a binary scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measuring fixture quality:&lt;/strong&gt; Track fixture fidelity and coverage instead of focusing solely on the model's score.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Problem #2: Sometimes the Judge Is Wrong
&lt;/h2&gt;

&lt;p&gt;Now imagine a different scenario.&lt;/p&gt;

&lt;p&gt;You have a clean, unambiguous fixture. The candidate model produces a response that is clearly correct. You pass it to your LLM judge. The judge gives it a score of 4 out of 5.&lt;/p&gt;

&lt;p&gt;You run the exact same evaluation five minutes later. The judge gives it a score of 5 out of 5.&lt;/p&gt;

&lt;p&gt;Nothing in the candidate response changed, yet the score did.&lt;/p&gt;

&lt;p&gt;Statisticians call this &lt;strong&gt;epistemic uncertainty&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here the opposite is true. The task is stable. The measurement isn't. That means your evaluation infrastructure—not your benchmark—is what needs improvement.&lt;/p&gt;

&lt;p&gt;Epistemic noise creeps in through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stochastic API Behavior:&lt;/strong&gt; Even at temperature 0, repeated calls to hosted LLM APIs can produce different outputs in practice because providers may use implementation details that are opaque to users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rubric Ambiguity:&lt;/strong&gt; If your prompt asks the judge, &lt;em&gt;"Is this response helpful?"&lt;/em&gt;, the judge is forced to invent its own definition of "helpful" on every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Biases:&lt;/strong&gt; LLMs exhibit self-preference (favoring their own text), position bias (preferring the first option), and length bias (favoring longer responses).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to Mitigate Judge Uncertainty
&lt;/h3&gt;

&lt;p&gt;In practice, hardening your evaluation framework against judge uncertainty comes down to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Judge ensembling:&lt;/strong&gt; If evaluation results influence production decisions, judge ensembling can substantially improve robustness. Run evaluations across multiple models (e.g., Claude, GPT-4, Gemini) to smooth out model-specific blind spots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measuring inter-judge agreement:&lt;/strong&gt; Track agreement rates (like Cohen’s Kappa) to identify when judges diverge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking judge variance:&lt;/strong&gt; Periodically run the exact same input through your judges multiple times to calculate your evaluation system's baseline standard deviation. If your benchmark reports a model score of 84%, the most useful number may not be 84. It may be &lt;strong&gt;84% ± 3%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-axis scoring:&lt;/strong&gt; Score along distinct, objective axes (e.g., correctness, safety, conciseness) using binary or highly structured scales instead of subjective 1-to-5 ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flagging judge-sensitive fixtures:&lt;/strong&gt; Automatically flag fixtures that show unusually high score variance across runs for manual auditing.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This distinction shapes the architecture of your evaluation system. If you don't know which type of noise you are dealing with, you will make the wrong engineering trade-offs. Spending weeks tweaking a prompt to evaluate an inherently ambiguous fixture is a waste of time.&lt;/p&gt;

&lt;p&gt;Once we started thinking about uncertainty this way, several architectural decisions became obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixture Audits&lt;/strong&gt; reduced task ambiguity by improving realism, coverage, and multi-turn representation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Axis Scoring&lt;/strong&gt; made disagreements explainable by separating quality dimensions rather than collapsing everything into a single score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge Ensembling&lt;/strong&gt; reduced judge uncertainty by comparing independent evaluators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence Reporting&lt;/strong&gt; allowed us to distinguish genuine regressions from expected evaluation variance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage Metrics&lt;/strong&gt; communicated the statistical confidence of the evaluation itself.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Moving Beyond the Scoreboard
&lt;/h2&gt;

&lt;p&gt;Traditional testing is about proving correctness.&lt;/p&gt;

&lt;p&gt;LLM evaluation is about estimating confidence.&lt;/p&gt;

&lt;p&gt;That distinction changes everything. It changes how we write fixtures, design judges, and interpret regressions. Most importantly, it changes what a successful evaluation system looks like.&lt;/p&gt;

&lt;p&gt;Reliable evaluation isn't about removing uncertainty.&lt;/p&gt;

&lt;p&gt;It's about understanding it well enough to make better engineering decisions.&lt;/p&gt;

&lt;p&gt;As LLM systems become more capable, our evaluation systems will need to become more sophisticated as well. The challenge is no longer simply measuring quality—it's measuring how confident we should be in the measurement itself. Once you embrace that shift, evaluation stops being a scoreboard and starts becoming a decision-making tool.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>evaluation</category>
      <category>architecture</category>
    </item>
    <item>
      <title>More Threads, Less Throughput: When AI Servers Fight the Scheduler</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Sat, 04 Jul 2026 15:45:46 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/more-threads-less-throughput-when-ai-servers-fight-the-scheduler-3oif</link>
      <guid>https://dev.to/travelingwilbur/more-threads-less-throughput-when-ai-servers-fight-the-scheduler-3oif</guid>
      <description>&lt;p&gt;Our inference server had 24 CPU cores, a mostly idle GPU, and only 10 concurrent requests.&lt;/p&gt;

&lt;p&gt;Somehow, average latency exceeded eight seconds.&lt;/p&gt;

&lt;p&gt;That shouldn't have been possible.&lt;/p&gt;

&lt;p&gt;We were running a Rails background worker system that dispatched embedding and classification requests to an internal Python inference service. As our background queue grew, we did what any sensible team would do: we increased the concurrency of our background workers and scaled up the inference web server.&lt;/p&gt;

&lt;p&gt;Instead of seeing our throughput scale linearly, the system ground to a halt. Latency spiked from milliseconds to over eight seconds, background queues backed up, and our server's CPU utilization pegged at 100%. Yet, looking at the GPU telemetry, it was practically idling, waiting for work.&lt;/p&gt;

&lt;p&gt;The CPUs weren't spending their time multiplying matrices. They were spending it deciding which thread should run next. As we dug deeper, we realized we were victims of a hidden performance killer: &lt;strong&gt;thread oversubscription&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Benchmark That Didn't Make Sense
&lt;/h2&gt;

&lt;p&gt;To diagnose the bottleneck, we isolated the inference service and ran a controlled load test. We simulated a modest concurrency of 10 concurrent requests, sending a total of 100 API requests to the Python inference server.&lt;/p&gt;

&lt;p&gt;While an individual, isolated inference request took around 912 milliseconds, putting a concurrent load of just 10 requests pushed the average response time out to over 8 seconds. Even worse, the CPU cores were completely saturated, but the GPU was only at 15% utilization.&lt;/p&gt;

&lt;p&gt;In a healthy system, if the CPU is at 100%, throughput should be maximized. But here, the CPU was working incredibly hard to produce almost no output.&lt;/p&gt;

&lt;p&gt;Here is what the initial baseline benchmark looked like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.28 req/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.43 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inference Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;912 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Active Threads&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~240&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Utilization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Our Initial Hypotheses
&lt;/h2&gt;

&lt;p&gt;We thought it was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU Saturation&lt;/strong&gt;: The model was too heavy for our hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insufficient Workers&lt;/strong&gt;: We needed to increase Rails concurrency to queue more work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow Model&lt;/strong&gt;: The embedding algorithm itself was inherently slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Overhead&lt;/strong&gt;: The data transfer between Rails and the inference API was lagging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It turned out to be none of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Following the CPU: The 240-Thread Discovery
&lt;/h2&gt;

&lt;p&gt;We jumped onto the inference server during the load test and ran &lt;code&gt;htop&lt;/code&gt; to see what the CPU was actually doing. What we saw was a wall of red and green bars representing 24 individual CPU cores (48 logical threads) pegged to their absolute limits.&lt;/p&gt;

&lt;p&gt;Then we checked the active thread count for the Python inference process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Count the number of lightweight processes (threads) for our service pid&lt;/span&gt;
ps &lt;span class="nt"&gt;-o&lt;/span&gt; nlwp &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;PID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was &lt;strong&gt;240&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A single process handling 10 concurrent web requests had spawned 240 active threads. Where did they come from?&lt;/p&gt;

&lt;p&gt;Our Python code was simple. It was a standard FastAPI application using Uvicorn. We weren't manually spawning threads. We were just loading a sentence-transformer model using PyTorch and running inference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The seemingly innocent endpoint
&lt;/span&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/embed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Under the hood, this call is not single-threaded
&lt;/span&gt;    &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The culprit wasn't our application. It was the stack of native libraries underneath it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Native Libraries Lie to You
&lt;/h2&gt;

&lt;p&gt;To understand why 240 threads were running, we have to look at the architectural layers beneath PyTorch. &lt;/p&gt;

&lt;p&gt;When you call &lt;code&gt;model.encode()&lt;/code&gt; in Python, your request travels down a vertical stack of abstractions before hitting the CPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Request
                   │
         FastAPI / Uvicorn
                   │
              PyTorch
                   │
      OpenMP / MKL / BLAS
                   │
            OS Scheduler
                   │
              CPU Cores
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the top, we have FastAPI/Uvicorn, which handles concurrent web connections. Beneath it is PyTorch, which coordinates the neural network graph execution. But PyTorch itself doesn't actually perform the core matrix arithmetic (like matrix multiplication). For that, it delegates to low-level, highly optimized native C and C++ math libraries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BLAS (Basic Linear Algebra Subprograms)&lt;/strong&gt;: The standardized API specification for low-level vector and matrix operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenBLAS / MKL (Intel Math Kernel Library)&lt;/strong&gt;: Concrete, highly optimized implementations of the BLAS API. They use hand-crafted assembly code, CPU vector extensions (like AVX-512), and internal threading to run matrix math at the absolute limits of the physical silicon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenMP (Open Multi-Processing)&lt;/strong&gt;: The concurrency engine used by these math libraries to split loops and parallelize matrix calculations across multiple threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These native libraries were designed for High-Performance Computing (HPC) environments, where a single program runs on a dedicated machine and expects to utilize every core for a single calculation. MKL and OpenMP query the host core count and spawn a worker thread pool matching it. They assume they own the hardware.&lt;/p&gt;

&lt;p&gt;While this behavior is perfect for a Jupyter notebook running on a developer's workstation, it is hostile in a web server or background job runner. Web servers scale by handling independent requests concurrently using separate processes or threads.&lt;/p&gt;

&lt;p&gt;If Uvicorn or Rails runs 10 workers on our 24-core server, each worker process makes its own PyTorch calls. PyTorch delegates to OpenMP, which independently spawns 24 threads per request. None of the workers are aware of each other.&lt;/p&gt;

&lt;p&gt;Repeat this for all 10 concurrent requests, and the math compounding is brutal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 Concurrent Requests 
  × 24 Threads per Request 
  = 240 Active Threads competing for 24 Physical Cores
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To visualize the scale of this duplication:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4rkjzx42bnkggp2ia87.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4rkjzx42bnkggp2ia87.png" alt=" " width="686" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The native libraries lied to us by assuming they were alone. By defaulting to the total core count, they optimized for single-task speed at the expense of multi-task system throughput. Instead of cooperating, the threads began to fight.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cost of Concurrency: Scheduler Contention
&lt;/h2&gt;

&lt;p&gt;When you have 240 active threads screaming for time on 24 physical CPU cores, the operating system is forced to step in. The OS scheduler must slice up CPU time and constantly swap threads in and out of the CPU registers.&lt;/p&gt;

&lt;p&gt;This is known as &lt;strong&gt;thread oversubscription&lt;/strong&gt;, and it introduces a massive overhead called &lt;strong&gt;scheduler contention&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxi1phvkya1jcrkfuierk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxi1phvkya1jcrkfuierk.png" alt=" " width="659" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every context switch is work the application didn't ask for. The scheduler saves registers, restores another thread's state, and the CPU begins executing a completely different workload. Caches become less effective, memory must be fetched again, and the processor spends less time multiplying matrices and more time preparing to multiply matrices.&lt;/p&gt;

&lt;p&gt;When the cache is constantly invalidated, the CPU cores spend most of their time waiting for data to be fetched from main memory (RAM) instead of performing floating-point math. The CPU is "busy," but it is busy managing its own metadata rather than executing your code.&lt;/p&gt;

&lt;p&gt;In our case, the scheduler contention was so severe that the overhead of coordinating the 240 threads completely wiped out the benefits of parallelizing the matrix math. Ten requests, each trying to run on 24 threads, took 912ms. If they had run sequentially on a single thread each, they would have finished in a fraction of the time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Global Concurrency Coordination
&lt;/h2&gt;

&lt;p&gt;The solution was counterintuitive but incredibly simple: we had to force the native libraries to stop parallelizing internal operations. We wanted each request to run on exactly &lt;strong&gt;one thread&lt;/strong&gt;, allowing the web server's concurrency model (10 worker processes) to match the physical hardware limits.&lt;/p&gt;

&lt;p&gt;We added the following configuration to the very top of our application entry point, before any deep learning library was imported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="c1"&gt;# Limit OpenMP threads
&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OMP_NUM_THREADS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MKL_NUM_THREADS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENBLAS_NUM_THREADS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VECLIB_MAXIMUM_THREADS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NUMEXPR_NUM_THREADS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Force PyTorch to use a single thread for intra-op and inter-op parallelism
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_num_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_num_interop_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By setting these variables to &lt;code&gt;1&lt;/code&gt;, we instructed PyTorch and its underlying math libraries to execute operations sequentially on a single thread per request. &lt;/p&gt;

&lt;p&gt;We then restarted our server and ran the exact same load test of 10 concurrent requests. &lt;/p&gt;

&lt;p&gt;The results were night and day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline (Default Threads)&lt;/th&gt;
&lt;th&gt;Optimized (1 Thread)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.28 req/s&lt;/td&gt;
&lt;td&gt;11.45 req/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+57%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.43 s&lt;/td&gt;
&lt;td&gt;5.24 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-38%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inference Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;912 ms&lt;/td&gt;
&lt;td&gt;560 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-38%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Active Threads&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~240&lt;/td&gt;
&lt;td&gt;~10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95% Reduction&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Utilization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Slashed in Half&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By reducing the thread count from 240 to 10, throughput increased by 57%, average latency dropped by nearly 4 seconds, and CPU utilization was cut in half. &lt;/p&gt;

&lt;p&gt;The CPU was no longer thrashing. It spent its cycles executing matrix multiplication instead of shuffling threads in and out of registers.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Multi-Threaded Math Libraries Are Actually Good
&lt;/h2&gt;

&lt;p&gt;This doesn't mean &lt;code&gt;OMP_NUM_THREADS=1&lt;/code&gt; is always correct. It is a design decision based entirely on your application's concurrency model.&lt;/p&gt;

&lt;p&gt;If you are running an offline batch job, training a model, or running a single-threaded daemon processing one task at a time, you &lt;em&gt;want&lt;/em&gt; PyTorch to use all available cores. In this scenario (data-level concurrency), letting MKL parallelize the matrix calculations makes your process run as fast as possible.&lt;/p&gt;

&lt;p&gt;However, if you are running a web server or background workers processing many independent requests in parallel (request-level concurrency), each process should use exactly &lt;code&gt;1&lt;/code&gt; thread. The operating system is already parallelizing the work across the cores at the process level. Adding internal library threads only causes them to fight for the scheduler's attention.&lt;/p&gt;

&lt;p&gt;If you run both workloads on the same server, you must configure them separately. For example, keep &lt;code&gt;OMP_NUM_THREADS=1&lt;/code&gt; on your web servers, but allow it to scale to the core count on your batch processing workers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Broader Engineering Lesson
&lt;/h2&gt;

&lt;p&gt;Every layer of the stack was trying to optimize itself independently. None of them were wrong. Together, they were inefficient.&lt;/p&gt;

&lt;p&gt;Modern web frameworks and background workers are designed to scale by running multiple isolated processes or threads. Native math libraries are also designed to scale by running multiple threads. If you don't coordinate these two layers, they will fight each other for the scheduler's attention.&lt;/p&gt;

&lt;p&gt;Systems don't become fast because every component is individually optimized. They become fast when every component cooperates.&lt;/p&gt;

&lt;p&gt;Before you scale your servers horizontally or buy larger GPUs, inspect your process thread count. The best performance optimizations don't always come from writing faster code—sometimes they come from simply stopping your libraries from fighting each other.&lt;/p&gt;

&lt;p&gt;No magic. Just systems.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>systems</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop Sending Embeddings as JSON: A Faster Binary Serialization Pattern for AI APIs</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Sat, 04 Jul 2026 13:51:37 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/stop-sending-embeddings-as-json-a-faster-binary-serialization-pattern-for-ai-apis-e86</link>
      <guid>https://dev.to/travelingwilbur/stop-sending-embeddings-as-json-a-faster-binary-serialization-pattern-for-ai-apis-e86</guid>
      <description>&lt;p&gt;Every embedding API I've worked with sends vectors as JSON.&lt;/p&gt;

&lt;p&gt;That's convenient.&lt;/p&gt;

&lt;p&gt;It's also one of the most expensive ways to move numerical data between services. JSON isn't expensive simply because it's text. It's expensive because every floating-point value must be converted from binary to decimal, transmitted as characters, and parsed back into binary on the receiving side. You pay that cost on every dimension of every vector, in both directions.&lt;/p&gt;

&lt;p&gt;By replacing JSON arrays with Base64-encoded binary float buffers, we reduced payload size by &lt;strong&gt;75%&lt;/strong&gt; and cut serialization latency by over &lt;strong&gt;98%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the benchmark, measured against 1,000 embeddings at 1,536 dimensions each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;JSON Float Array&lt;/th&gt;
&lt;th&gt;Base64 Float Buffer&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Payload Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;120.3 MB&lt;/td&gt;
&lt;td&gt;30.1 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-75.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python Serialization Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3,140 ms&lt;/td&gt;
&lt;td&gt;45 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-98.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ruby Deserialization Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4,820 ms&lt;/td&gt;
&lt;td&gt;110 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-97.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Roundtrip Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.42 s&lt;/td&gt;
&lt;td&gt;1.84 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-80.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Benchmark environment:&lt;/strong&gt; Ruby 3.3.4, Python 3.12, NumPy 1.26, FastAPI 0.111, Apple M3 Pro (development) and x86_64 Linux (production). Each format was measured over 1,000 iterations with GC disabled during measurement using Ruby's &lt;code&gt;Benchmark::IPS&lt;/code&gt; and Python's &lt;code&gt;timeit&lt;/code&gt;. Payload sizes were measured as raw HTTP body bytes before compression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproduce it yourself:&lt;/strong&gt; The &lt;a href="https://gist.github.com/wilburhimself/9acedeb7bc3c90606450f0db8aa070b3" rel="noopener noreferrer"&gt;Python serialization and Ruby deserialization benchmark scripts&lt;/a&gt; are available as a Gist. Clone, run, and compare on your own hardware.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep reading for the explanation, the implementation, and the cases where JSON is still the right answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Numerical Data Doesn't Belong in JSON
&lt;/h2&gt;

&lt;p&gt;Computers do not store floating-point numbers as text. In memory, a single-precision float (&lt;code&gt;float32&lt;/code&gt; in Python, &lt;code&gt;float&lt;/code&gt; in C) is stored as a 4-byte (32-bit) binary value.&lt;/p&gt;

&lt;p&gt;When you serialize a float to JSON, you pay a conversion cost in both directions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON pipeline (what everyone does):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;float32 in memory (4 bytes)
    ↓  binary-to-decimal conversion
"-0.23142091" as ASCII string (11+ bytes)
    ↓  HTTP (24 KB per 1,536-dim vector)
JSON parser reads characters
    ↓  string-to-float conversion (strtof)
float in Ruby memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Binary pipeline (what we switched to):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;float32 in memory (4 bytes)
    ↓  tobytes() — zero conversion, O(1)
raw bytes
    ↓  Base64 encode (fast C implementation)
ASCII string (8 KB per 1,536-dim vector)
    ↓  HTTP
Base64 decode → unpack("e*") in Ruby
    ↓  single C pass over raw bytes
float in Ruby memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The math is straightforward: in binary, a &lt;code&gt;float32&lt;/code&gt; occupies exactly &lt;strong&gt;4 bytes&lt;/strong&gt;. In JSON, the same number &lt;code&gt;-0.23142091&lt;/code&gt; occupies &lt;strong&gt;11 bytes as text&lt;/strong&gt; plus separators—a 3x–4x size multiplier.&lt;/p&gt;

&lt;p&gt;For a single 1,536-dimension embedding, a binary payload is &lt;strong&gt;6,144 bytes&lt;/strong&gt; (~6 KB). The equivalent JSON array exceeds &lt;strong&gt;24 KB&lt;/strong&gt;. Multiply across a batch of 50 documents and you're transmitting over a megabyte of ASCII where 300 KB of binary would have been sufficient.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnaosczdbzs1kgukl7nb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnaosczdbzs1kgukl7nb.png" alt=" " width="716" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The network payload is only part of the problem. The CPU cost is often worse. To produce the JSON, Python must format each float as a string and concatenate them; Ruby must then locate delimiters, allocate memory for each token, and run string-to-float conversion for every dimension. That loop scales linearly with batch size and embedding dimensions, and at high throughput it dominates request latency.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For services generating a few embeddings per minute, JSON is perfectly adequate. The break-even point depends on batch sizes and hardware; profile before optimizing.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Naive Solution (and Why it Fails at Scale)
&lt;/h2&gt;

&lt;p&gt;Here is how our original system looked.&lt;/p&gt;

&lt;p&gt;On the Python inference side (FastAPI), we returned embeddings using standard NumPy-to-list conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# FastAPI endpoint (Slow &amp;amp; Heavy)
&lt;/span&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RequestPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# numpy array of float32
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# This implicitly converts the numpy array to a list of Python floats,
&lt;/span&gt;        &lt;span class="c1"&gt;# which FastAPI's JSON encoder serializes to a string.
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Rails side, we used standard HTTP clients and JSON parsing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rails Client (Slow &amp;amp; Heavy)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InferenceClient&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;"http://inference-server/v1/embeddings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;body: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;texts: &lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;headers: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# JSON parsing allocates thousands of strings and converts them back to floats&lt;/span&gt;
    &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"embeddings"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During profiling, we discovered that Rails spent up to &lt;strong&gt;40% of its execution time&lt;/strong&gt; inside &lt;code&gt;JSON.parse&lt;/code&gt; when processing large batches of embeddings. We were spending more CPU power parsing strings than executing business logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Better Approach: Binary Float Buffers via Base64
&lt;/h2&gt;

&lt;p&gt;Rather than trying to parse JSON faster, we eliminated JSON serialization of the embedding vector itself.&lt;/p&gt;

&lt;p&gt;We serialize the float array as a raw binary buffer, then encode that buffer into a single Base64 string. Base64 introduces about 33% size overhead compared to raw binary, but it lets us embed binary data safely inside standard JSON payloads—no changes to content-type negotiation, no custom binary HTTP framing, no breaking changes to existing API consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Python Inference Server (Encoding)
&lt;/h3&gt;

&lt;p&gt;Instead of calling &lt;code&gt;.tolist()&lt;/code&gt;, we extract the raw bytes from the underlying C-contiguous memory block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serialize_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Ensure the array is single-precision float32
&lt;/span&gt;    &lt;span class="n"&gt;float_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Get raw C-compatible memory bytes
&lt;/span&gt;    &lt;span class="n"&gt;raw_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;float_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Base64 encode the bytes and decode to an ASCII string
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_bytes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ascii&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our API response now returns a single string instead of an array of numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"embedding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MzMzMzPz8/M+MzMzMzPz8z8zMzMz..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Rails Application Server (Decoding)
&lt;/h3&gt;

&lt;p&gt;On the Ruby side, we decode the Base64 string back into raw bytes, then unpack them into Ruby Floats using &lt;code&gt;String#unpack&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"base64"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingDecoder&lt;/span&gt;
  &lt;span class="c1"&gt;# Decodes a Base64-encoded float32 binary buffer into a Ruby array of floats&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Decode Base64 string back to binary string (raw bytes)&lt;/span&gt;
    &lt;span class="n"&gt;binary_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict_decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Unpack the binary buffer.&lt;/span&gt;
    &lt;span class="c1"&gt;# 'e' = little-endian single-precision (32-bit) float&lt;/span&gt;
    &lt;span class="c1"&gt;# '*' = unpack all remaining data in the string&lt;/span&gt;
    &lt;span class="n"&gt;binary_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"e*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is &lt;code&gt;unpack("e*")&lt;/code&gt;. Unlike iterating over a Ruby array, &lt;code&gt;unpack&lt;/code&gt; runs entirely in optimized C inside the Ruby VM. It reads raw bytes directly from memory, offsets the pointer by 4 bytes at a time, and constructs the corresponding Ruby Float objects in a single pass—no string parsing, no delimiter scanning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Under the Hood: Endianness and the &lt;code&gt;unpack&lt;/code&gt; Directive
&lt;/h2&gt;

&lt;p&gt;When moving binary data across languages and hardware, two things matter: &lt;strong&gt;precision&lt;/strong&gt; and &lt;strong&gt;endianness&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Precision (Float32 vs. Float64)
&lt;/h3&gt;

&lt;p&gt;NumPy defaults to &lt;code&gt;float64&lt;/code&gt; (8 bytes per float) for many operations, but AI embeddings are almost universally &lt;code&gt;float32&lt;/code&gt; (4 bytes per float). We explicitly call &lt;code&gt;.astype(np.float32)&lt;/code&gt; before serializing. If you serialize as &lt;code&gt;float64&lt;/code&gt; and unpack as &lt;code&gt;float32&lt;/code&gt;, the decoded numbers are meaningless.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Endianness (Byte Ordering)
&lt;/h3&gt;

&lt;p&gt;Endianness determines the order bytes are written to memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Little-Endian&lt;/strong&gt;: Least significant byte first. Used by x86_64 and ARM (Apple Silicon, modern Linux ARM).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big-Endian&lt;/strong&gt;: Most significant byte first. Used by network protocols and older RISC architectures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ruby's &lt;code&gt;String#unpack&lt;/code&gt; directives are explicit about this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;f*&lt;/code&gt; — native CPU endianness (fragile across architectures)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;g*&lt;/code&gt; — big-endian single-precision floats&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;e*&lt;/code&gt; — little-endian single-precision floats&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We use &lt;code&gt;e*&lt;/code&gt; because both our Python inference servers (x86 Linux) and Rails app servers (x86 Linux and ARM macOS) are little-endian. More importantly, being explicit about endianness means the decoding is correct by definition, regardless of what machine runs the code in the future.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tradeoffs
&lt;/h2&gt;

&lt;p&gt;This optimization is not universal. Here's an honest accounting of the alternatives we evaluated before landing here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not gRPC or Protocol Buffers?
&lt;/h3&gt;

&lt;p&gt;Protocol Buffers with gRPC is the canonical answer to this problem in infrastructure-heavy organizations. It solves serialization, adds schema evolution, streaming, and bidirectional communication. We evaluated it. The operational cost—generated stubs for every language, strict schema registration, gRPC infrastructure—was high for an internal service consumed in one place. Base64 binary over HTTP let us ship in an afternoon with no new infrastructure. For systems with multiple consumers or where schema evolution matters, protobuf is the better long-term answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not MessagePack?
&lt;/h3&gt;

&lt;p&gt;MessagePack is an excellent binary serialization format and would have worked here. In our case, the embedding vector is the only binary payload in the response—everything else is standard JSON. Introducing a full MessagePack codec for a single field felt like over-engineering. If you're serializing entire response objects in binary, MessagePack or protobuf is more appropriate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not Apache Arrow?
&lt;/h3&gt;

&lt;p&gt;Arrow's columnar in-memory format is optimized for batch numerical data and enables zero-copy access in many scenarios. For a dedicated ML pipeline where Ruby is doing heavy vector operations, Arrow (via the &lt;code&gt;red-arrow&lt;/code&gt; gem) is worth evaluating. In our case, embeddings pass directly into pgvector; the overhead of Arrow's columnar framing wasn't justified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not raw binary HTTP?
&lt;/h3&gt;

&lt;p&gt;A pure binary HTTP response (&lt;code&gt;Content-Type: application/octet-stream&lt;/code&gt;) would eliminate the ~33% Base64 overhead. We opted against it because it requires custom content-type handling on both sides, breaks standard HTTP debugging tooling, and complicates response envelope composition—status codes, metadata, and errors all live in the same JSON body. Base64-in-JSON is a pragmatic middle ground: it keeps the API contract intact while avoiding the text serialization cost for the high-volume field.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real costs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Loss of human readability.&lt;/strong&gt; You can no longer inspect embedding values in curl or Chrome DevTools. Debugging requires decoding the Base64 string out-of-band.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tight schema coupling.&lt;/strong&gt; Changing precision from &lt;code&gt;float32&lt;/code&gt; to &lt;code&gt;float16&lt;/code&gt; or &lt;code&gt;float64&lt;/code&gt; on the Python side requires a simultaneous update to the Ruby unpack directive. There is no self-describing schema to catch mismatches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Array allocation overhead.&lt;/strong&gt; &lt;code&gt;unpack&lt;/code&gt; is fast, but Ruby still allocates 1,536 Float objects per vector. If you're passing embeddings directly into pgvector, benchmark whether you need to materialize the Ruby array at all—some clients accept binary blobs directly.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion: Text is for Humans, Binary is for Systems
&lt;/h2&gt;

&lt;p&gt;JSON has won because it's readable. For most web APIs, that readability is worth the overhead—payloads are small and parsing costs are negligible.&lt;/p&gt;

&lt;p&gt;Embeddings are different. A single vector from a modern model contains more numerical data than most entire API responses. When you serialize dense numerical arrays as strings, you're not just adding overhead—you're mismatching the data structure to its representation.&lt;/p&gt;

&lt;p&gt;Performance work often starts with algorithms, but the largest wins frequently come from changing representations instead. Once you stop treating dense numerical data like text, the CPU suddenly has much less work to do.&lt;/p&gt;

&lt;p&gt;That principle extends well beyond embeddings. Whenever you find yourself serializing audio buffers, image histograms, or numerical feature vectors as JSON, stop and ask whether the text representation is serving the system or just the developer who wrote the first version.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Set-Based Updates in Rails: 4 Hours to 8 Seconds</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Tue, 12 May 2026 14:18:50 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/set-based-updates-in-rails-4-hours-to-8-seconds-dkd</link>
      <guid>https://dev.to/travelingwilbur/set-based-updates-in-rails-4-hours-to-8-seconds-dkd</guid>
      <description>&lt;p&gt;I once inherited a background job to deactivate stale users. In production, a job processing 50,000 users that should have taken a minute was taking over 4 hours, consuming 2GB of RAM, and frequently timing out.&lt;/p&gt;

&lt;p&gt;The culprit was a classic Rails performance pitfall: the N+1 update loop. The code looked innocent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Find users whose last login was more than 90 days ago&lt;/span&gt;
&lt;span class="n"&gt;users_to_deactivate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"last_login_at &amp;lt; ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;users_to_deactivate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# This runs one UPDATE query for every single user&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;active: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a flood of queries, hammering the database with thousands of individual transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The N+1 Update Hell&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_login_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2025-11-30...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="nv"&gt;"active"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"updated_at"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="nv"&gt;"active"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"updated_at"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ... 49,998 more UPDATE statements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is not just inefficient; it's hostile to your database. There is a much better way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A Note on Indexes&lt;/strong&gt;&lt;br&gt;
All performance advice in this post assumes your query conditions are indexed. The &lt;code&gt;User.where("last_login_at &amp;lt; ?", ...)&lt;/code&gt; query is only fast if you have a database index on the &lt;code&gt;last_login_at&lt;/code&gt; column. Without it, the &lt;code&gt;SELECT&lt;/code&gt; itself will be slow.&lt;/p&gt;

&lt;p&gt;You can check this with &lt;code&gt;User.where("last_login_at &amp;lt; ?", 90.days.ago).explain&lt;/code&gt;. If you see a &lt;code&gt;Seq Scan&lt;/code&gt; (Sequential Scan), you need an index:&lt;/p&gt;


&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# db/migrate/YYYYMMDDHHMMSS_add_index_to_users_last_login_at.rb&lt;/span&gt;
&lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_login_at&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Power of &lt;code&gt;update_all&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of pulling 50,000 records into memory, we can tell the database to update the entire set in a single command. Active Record's &lt;code&gt;update_all&lt;/code&gt; constructs one SQL &lt;code&gt;UPDATE&lt;/code&gt; statement and sends it directly to the database.&lt;/p&gt;

&lt;p&gt;Let's refactor the slow job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"last_login_at &amp;lt; ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;active: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"Deactivated &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; users."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates one beautiful, efficient SQL query and returns the number of rows affected. The result: 4 hours becomes 8 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Sharp Edges of &lt;code&gt;update_all&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This performance comes with a critical trade-off: &lt;code&gt;update_all&lt;/code&gt; bypasses most of the ActiveRecord lifecycle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;It ignores validations.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;It skips callbacks.&lt;/strong&gt; This will break any business logic in &lt;code&gt;after_save&lt;/code&gt; or &lt;code&gt;after_commit&lt;/code&gt;, like sending notifications or invalidating caches.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The &lt;code&gt;updated_at&lt;/code&gt; Trap
&lt;/h4&gt;

&lt;p&gt;The most insidious side effect is that &lt;strong&gt;&lt;code&gt;update_all&lt;/code&gt; does not automatically update the &lt;code&gt;updated_at&lt;/code&gt; timestamp.&lt;/strong&gt; This will break any downstream system that relies on it for cache invalidation, audit trails, or synchronization.&lt;/p&gt;

&lt;p&gt;Imagine a UI that caches a user's profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use &lt;code&gt;update_all(active: false)&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt; remains unchanged. The cache key stays the same, and your UI continues to show the user as active until the cache expires. The fix is to set it manually, as shown in the example above.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Middle Ground: &lt;code&gt;update_columns&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;update_columns&lt;/code&gt; is for when you &lt;em&gt;already have&lt;/em&gt; an object in memory and need to perform a targeted update while bypassing callbacks and validations. It's a scalpel for a single record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"some_user@example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ... some complex logic ...&lt;/span&gt;

&lt;span class="c1"&gt;# Now, update just one attribute without triggering callbacks.&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;login_attempts: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike &lt;code&gt;update_all&lt;/code&gt;, &lt;code&gt;update_columns&lt;/code&gt; &lt;em&gt;does&lt;/em&gt; touch &lt;code&gt;updated_at&lt;/code&gt; by default (though this can be configured). It is &lt;strong&gt;not&lt;/strong&gt; for bulk operations; using it in a loop brings you right back to the N+1 problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  War Story #2: The Death-by-a-Thousand-Increments
&lt;/h3&gt;

&lt;p&gt;I once debugged a Rails app where a popular blog post page was taking 20 seconds to load. The culprit? The controller was incrementing view counts like this for every recommended article on the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ANTI-PATTERN: 50 posts on the page = 50 UPDATE queries&lt;/span&gt;
&lt;span class="n"&gt;recommended_posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:view_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;increment!&lt;/code&gt; is just &lt;code&gt;update&lt;/code&gt; under the hood. The fix was to switch to &lt;code&gt;update_all&lt;/code&gt; with a SQL fragment, which dropped the response time to 200ms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GOOD: One query to increment all counters&lt;/span&gt;
&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;recommended_posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"view_count = view_count + 1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Going Deeper: Safe Heterogeneous Updates with SQL &lt;code&gt;CASE&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;update_all&lt;/code&gt; is great for applying the &lt;em&gt;same&lt;/em&gt; change to many records. But what if you need to update a set of records, each with a &lt;em&gt;different&lt;/em&gt; value, like reordering items in a playlist?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Security Warning: SQL Injection&lt;/strong&gt;&lt;br&gt;
Building raw SQL queries with string interpolation is extremely dangerous. The following example demonstrates a &lt;strong&gt;safe&lt;/strong&gt; approach using &lt;code&gt;sanitize_sql_array&lt;/code&gt;. &lt;strong&gt;Never&lt;/strong&gt; inject raw user input directly into SQL strings.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use case: Reorder a list of tasks from user input&lt;/span&gt;
&lt;span class="c1"&gt;# updates = { "1" =&amp;gt; 1, "2" =&amp;gt; 2, "3" =&amp;gt; 3 } where key is task_id and value is new position&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reorder_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Build the CASE statement using sanitize_sql_array for each condition&lt;/span&gt;
  &lt;span class="n"&gt;case_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# Ensure id and position are integers to prevent injection&lt;/span&gt;
    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize_sql_array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"WHEN ? THEN ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Construct the final, safe update statement&lt;/span&gt;
  &lt;span class="n"&gt;update_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"position = CASE id &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;case_sql&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; END, updated_at = :now"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;now: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="no"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most performant way to handle complex batch updates, combining the power of raw SQL with the safety of Rails' sanitization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Considerations
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Locking and Concurrency
&lt;/h4&gt;

&lt;p&gt;A long-running &lt;code&gt;update_all&lt;/code&gt; can lock many rows, blocking other requests. This can cause timeouts in a high-traffic application. It's often better to process records in batches to reduce lock duration.&lt;/p&gt;

&lt;h4&gt;
  
  
  Processing in Batches
&lt;/h4&gt;

&lt;p&gt;For very large updates, use &lt;code&gt;in_batches&lt;/code&gt; to break the work into smaller chunks. This runs more queries but keeps transactions short and locks minimal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Process 50,000 users in batches of 5,000&lt;/span&gt;
&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"last_login_at &amp;lt; ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;in_batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;of: &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;active: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Optional: yield to other processes&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Decision Framework
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;update&lt;/code&gt;&lt;/strong&gt;: The default for single-record updates where callbacks and validations &lt;strong&gt;must&lt;/strong&gt; run.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;update_columns&lt;/code&gt;&lt;/strong&gt;: A scalpel for single-record updates where callbacks and validations &lt;strong&gt;must&lt;/strong&gt; be skipped.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;update_all&lt;/code&gt;&lt;/strong&gt;: The workhorse for bulk-updating many records to the &lt;strong&gt;same&lt;/strong&gt; value.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;update_all&lt;/code&gt; with &lt;code&gt;CASE&lt;/code&gt;&lt;/strong&gt;: The specialist tool for bulk-updating many records to &lt;strong&gt;different&lt;/strong&gt; values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid set-based updates entirely when you need to trigger side effects (like sending emails) for each updated record or when you need to run model validations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The next time you write &lt;code&gt;.each { |record| record.update(...) }&lt;/code&gt;, pause. Ask yourself: am I processing complex business logic for each record, or am I simply updating a set of records based on a condition?&lt;/p&gt;

&lt;p&gt;If you're just changing data, you're in set-based territory. Choosing the right tool for the job is the mark of a seasoned developer. Your database will thank you. And when your background job finishes in 8 seconds instead of timing out after 4 hours, you will too.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>sql</category>
      <category>performance</category>
      <category>ruby</category>
    </item>
    <item>
      <title>How CLAUDE.md actually works</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Sat, 09 May 2026 06:33:47 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/how-claudemd-actually-works-3l1e</link>
      <guid>https://dev.to/travelingwilbur/how-claudemd-actually-works-3l1e</guid>
      <description>&lt;p&gt;Most engineers write &lt;code&gt;CLAUDE.md&lt;/code&gt; like a README. They put their stack, a few preferences, maybe a note about testing. The agent reads it, nods politely, and proceeds to write code shaped by its training data rather than their codebase.&lt;/p&gt;

&lt;p&gt;The file exists. It doesn't do much.&lt;/p&gt;

&lt;p&gt;The problem isn't effort. It's category. A README describes a project. A specification constrains behavior. &lt;code&gt;CLAUDE.md&lt;/code&gt; only works as the second thing, and most engineers have never written a specification for an agent before, so they reach for the format they know.&lt;/p&gt;

&lt;p&gt;This post covers three dimensions of getting it right: how to structure the file, how to write rules the agent actually follows, and how to keep the file current as the codebase evolves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Structure: one file is the wrong model
&lt;/h2&gt;

&lt;p&gt;Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt; from the project root. What most engineers don't know is that it also reads &lt;code&gt;CLAUDE.md&lt;/code&gt; from any subdirectory it's working in, and those files inherit from the root.&lt;/p&gt;

&lt;p&gt;This matters because context is directional. The rules that apply when the agent is writing a migration are not the rules that apply when it's writing a Sidekiq job. A single root file that tries to encode both produces either redundancy or contradiction.&lt;/p&gt;

&lt;p&gt;A structure that works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── CLAUDE.md                    # global constraints, stack, communication protocol
├── app/
│   ├── models/CLAUDE.md         # schema rules, validation patterns, query constraints
│   ├── jobs/CLAUDE.md           # idempotency rules, retry logic, failure handling
│   └── services/CLAUDE.md       # when to extract, interface contracts, no god objects
└── db/
    └── migrate/CLAUDE.md        # migration rules, index requirements, reversibility
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file answers: what does good work look like in this specific part of the codebase?&lt;/p&gt;

&lt;p&gt;The root file handles things true everywhere: pinned versions, architectural decisions already made, the communication protocol (plan before execute, ask before touching schema). The directory files encode local expertise. When the agent opens &lt;code&gt;app/jobs/&lt;/code&gt;, it reads both. The local file wins on conflicts.&lt;/p&gt;

&lt;p&gt;Write the root file last. You'll know what belongs there after writing the directory files, because you'll see what keeps repeating.&lt;/p&gt;




&lt;h2&gt;
  
  
  Structure: separate facts from rules
&lt;/h2&gt;

&lt;p&gt;Inside any &lt;code&gt;CLAUDE.md&lt;/code&gt;, two categories of content live at different distances from the agent's decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Facts&lt;/strong&gt; describe the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Stack&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Ruby 3.3.4
&lt;span class="p"&gt;-&lt;/span&gt; Rails 7.2.1
&lt;span class="p"&gt;-&lt;/span&gt; PostgreSQL 15
&lt;span class="p"&gt;-&lt;/span&gt; Sidekiq 7.3
&lt;span class="p"&gt;-&lt;/span&gt; pgvector 0.7 (used in app/models/document.rb, not widely available)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rules&lt;/strong&gt; constrain behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Database&lt;/span&gt;
Never update records in a loop. Use set-based updates.
Never call update_all without a WHERE condition.
Every query touching the documents table needs an EXPLAIN ANALYZE before merging.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Engineers mix these in paragraph form and the agent treats them with roughly equal weight. Separate them with explicit headers. Facts get a section. Rules get a section. The agent applies rules when deciding; it consults facts when writing. Conflating them produces a file that's harder to maintain and rules that get buried.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content: negative rules outperform positive ones
&lt;/h2&gt;

&lt;p&gt;"Prefer set-based updates" loses to "never update records in a loop." Both encode the same intent. One requires the agent to weigh it against competing preferences. The other removes the decision entirely.&lt;/p&gt;

&lt;p&gt;Agents generalize from positive instructions; they treat negative ones as constraints. Constraints bind more consistently than preferences, especially when the agent is mid-task and optimizing for completion.&lt;/p&gt;

&lt;p&gt;The practical test: rewrite every "prefer X" in your &lt;code&gt;CLAUDE.md&lt;/code&gt; as "never Y." If you can't, the rule isn't specific enough to be useful.&lt;/p&gt;

&lt;p&gt;Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Weak&lt;/span&gt;
Prefer idiomatic Rails patterns where possible.

&lt;span class="gh"&gt;# Useful&lt;/span&gt;
Never extract a service object without a domain justification written in a comment above the class.
Never rescue StandardError without logging the exception and the calling context.
Never write a background job where perform is not safe to call twice with the same arguments.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first rule tells the agent something it already knows. The second, third, and fourth tell it something about your codebase specifically, stated as a constraint it can't argue with.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content: attach the why to every constraint
&lt;/h2&gt;

&lt;p&gt;A rule without a reason the agent follows literally. A rule with a reason it generalizes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Without why&lt;/span&gt;
Never use update_all without conditions.

&lt;span class="gh"&gt;# With why&lt;/span&gt;
Never use update_all without conditions. A bare update_all on the users table
has caused production data corruption twice in this codebase because background
jobs held stale references. Always scope it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both rules produce the same behavior on the exact scenario described. The second one produces correct behavior on the novel scenario the agent hasn't seen yet, because it understands the failure mode. The agent can ask "does this situation have the same risk?" and answer correctly.&lt;/p&gt;

&lt;p&gt;This is the highest-leverage thing you can do to &lt;code&gt;CLAUDE.md&lt;/code&gt;. Every rule that currently has no attached reason is a rule the agent interprets locally and fails to generalize.&lt;/p&gt;

&lt;p&gt;The format that works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gs"&gt;**Rule:**&lt;/span&gt; [constraint]
&lt;span class="gs"&gt;**Why:**&lt;/span&gt; [failure mode or past incident that produced the rule]
&lt;span class="gs"&gt;**Exception:**&lt;/span&gt; [when the rule doesn't apply, if any]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every rule needs a formal format. But every non-obvious rule needs a reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content: priority ordering for conflicting rules
&lt;/h2&gt;

&lt;p&gt;Rules conflict. An instruction to keep methods short conflicts with an instruction to avoid multiple database queries. An instruction to match existing patterns conflicts with an instruction to apply a new convention to all new files.&lt;/p&gt;

&lt;p&gt;Without explicit priority, the agent resolves conflicts by training data, not by your intent.&lt;/p&gt;

&lt;p&gt;Add a section near the top of the root file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Rule priority&lt;/span&gt;

When instructions conflict, apply them in this order:
&lt;span class="p"&gt;
1.&lt;/span&gt; Correctness — no silent failures, no data loss
&lt;span class="p"&gt;2.&lt;/span&gt; Schema integrity — migrations reviewed, constraints in place before code
&lt;span class="p"&gt;3.&lt;/span&gt; Explicit rules in this file — over Rails defaults, over conventions
&lt;span class="p"&gt;4.&lt;/span&gt; Rails conventions — over agent judgment
&lt;span class="p"&gt;5.&lt;/span&gt; Agent judgment — last resort only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds obvious written out. It isn't obvious to an agent mid-task, and the cost of the agent resolving a conflict incorrectly is a file it has already changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content: show examples inline
&lt;/h2&gt;

&lt;p&gt;Instructions describe intent. Examples demonstrate standard.&lt;/p&gt;

&lt;p&gt;For any rule where the wrong implementation is plausible, put both versions in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Background jobs&lt;/span&gt;

Jobs must survive duplicate execution. Test this by reading perform as if it runs twice in sequence with the same arguments.

Bad:

def perform(order_id)
  order = Order.find(order_id)
  order.update!(status: :processed)
  OrderMailer.confirmation(order).deliver_later
end&lt;span class="sb"&gt;


&lt;/span&gt;Good:

def perform(order_id)
  order = Order.find(order_id)
  return if order.processed?

  order.update!(status: :processed)
  OrderMailer.confirmation(order).deliver_later
end&lt;span class="sb"&gt;


&lt;/span&gt;The return guard is the idempotency checkpoint. Every job needs one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent calibrates to examples faster than to instructions. It can see the difference between the two implementations immediately; it can't always infer the difference from a prose rule alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Workflow: let the agent update the file
&lt;/h2&gt;

&lt;p&gt;This is the trick most engineers skip entirely.&lt;/p&gt;

&lt;p&gt;After any session where the agent encountered a pattern it didn't handle correctly, or where you corrected its output more than once, end the session with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before we close: what did you learn in this session that should be added to CLAUDE.md?
List specific rules or facts, not generalizations. Draft the additions in the format
already used in the file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent has context you don't at the end of a long session. It knows which rules it consulted, which ones it had to interpret ambiguously, and where it made a decision it wasn't confident about. It can surface those explicitly if you ask.&lt;/p&gt;

&lt;p&gt;Review what it produces. Reject what's too general. Add what's specific and accurate.&lt;/p&gt;

&lt;p&gt;This loop does two things. First, it keeps &lt;code&gt;CLAUDE.md&lt;/code&gt; current without requiring you to remember what happened three sessions ago. Second, it produces rules written in a format the agent understands, because the agent wrote them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Workflow: treat CLAUDE.md as a living specification under version control
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; should be in git. Commit messages should say why a rule changed, not just that it did.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add explicit idempotency requirement to jobs/CLAUDE.md

Background job wrote duplicate notifications in production on 2024-11-12
because the retry logic ran perform twice. Added rule with failure mode attached.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the git history of &lt;code&gt;CLAUDE.md&lt;/code&gt; the same way you'd read the git history of a schema file. It tells you what the codebase has learned about itself. A rule with no commit context is a rule you'll forget to defend when someone asks why it exists.&lt;/p&gt;

&lt;p&gt;A practical schedule: review &lt;code&gt;CLAUDE.md&lt;/code&gt; every Friday, same as any other configuration that controls production behavior. Ask three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which rules did the agent follow incorrectly this week? Rewrite them.&lt;/li&gt;
&lt;li&gt;Which decisions did we make that aren't encoded yet? Add them.&lt;/li&gt;
&lt;li&gt;Which rules are stale because the codebase has changed? Remove or update them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ten minutes. The agent's output quality is a direct function of how current the file is. Stale context produces output that was correct for the codebase you had six months ago.&lt;/p&gt;




&lt;h2&gt;
  
  
  Workflow: ask the agent to audit its own compliance
&lt;/h2&gt;

&lt;p&gt;At the start of a session where the work is consequential, run this before any task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read CLAUDE.md, then read the files you're about to modify.
Before writing any code, list:
1. Which rules in CLAUDE.md apply to this task
2. Any existing code in the target files that already violates those rules
3. Any ambiguity in the rules that you'll need me to resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This surfaces two problems before they compound: rules the agent would have applied inconsistently, and existing violations it would have implicitly extended by matching surrounding code.&lt;/p&gt;

&lt;p&gt;The audit takes thirty seconds. It prevents the situation where the agent writes new code that follows your rules while the file it's modifying already breaks them, and now you have an inconsistency with the agent's fingerprints on both sides of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The underlying model
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is a specification for a collaborator who has no memory between sessions, has read every Rails codebase that existed before its training cutoff, and defaults to the most common pattern when your instructions are ambiguous.&lt;/p&gt;

&lt;p&gt;The file's job is to replace "most common Rails pattern" with "this specific codebase's pattern." It can only do that job if the rules are negative and specific, the reasons are attached, the examples show the wrong and right version side by side, and the file is updated when the codebase learns something new.&lt;/p&gt;

&lt;p&gt;Write it like a contract your future self will have to enforce. Because every session, you're handing the codebase to someone who has never seen it before and has to be told everything that matters.&lt;/p&gt;

&lt;p&gt;The README version of &lt;code&gt;CLAUDE.md&lt;/code&gt; produces a generic collaborator. The specification version produces one that can be trusted with a schema migration.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
    </item>
    <item>
      <title>Writing Maintainable AI Prompts in Rails with Promptly</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Mon, 18 Aug 2025 22:03:48 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/writing-maintainable-ai-prompts-in-rails-with-promptly-1dnf</link>
      <guid>https://dev.to/travelingwilbur/writing-maintainable-ai-prompts-in-rails-with-promptly-1dnf</guid>
      <description>&lt;p&gt;A few months ago, I was helping a Rails team add AI-generated onboarding emails to their app. It seemed simple at first: drop an &lt;code&gt;OpenAI.chat(...)&lt;/code&gt; call inside a mailer, pass in the user’s name, and let the model draft a warm welcome.&lt;/p&gt;

&lt;p&gt;By the second week, things got messy.&lt;/p&gt;

&lt;p&gt;Different controllers and jobs had their own inline prompts.&lt;/p&gt;

&lt;p&gt;Some prompts were copy-pasted with slight tweaks (“friendly tone”, “professional tone”, etc.).&lt;/p&gt;

&lt;p&gt;Marketing wanted localized versions for Spanish and Portuguese.&lt;/p&gt;

&lt;p&gt;QA asked, “How do we know the prompts didn’t change by accident?”&lt;/p&gt;

&lt;p&gt;We ended up with prompt spaghetti—hard to test, impossible to translate cleanly, and brittle whenever we tried to refactor.&lt;/p&gt;

&lt;p&gt;That experience sparked the idea for &lt;a href="https://github.com/wilburhimself/promptly" rel="noopener noreferrer"&gt;Promptly&lt;/a&gt;: a small gem that brings Rails conventions to AI prompt management.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problem: Prompts Don’t Belong Scattered in Code
&lt;/h3&gt;

&lt;p&gt;Rails developers already know the pain of hard-coded strings—why we use views, partials, and I18n instead of littering messages across controllers. Yet many of us are now hardcoding AI prompts directly in service objects or background jobs.&lt;/p&gt;

&lt;p&gt;It doesn’t scale.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplication: The same wording lives in multiple places.&lt;/li&gt;
&lt;li&gt;Localization headaches: Inline strings don’t play well with I18n.&lt;/li&gt;
&lt;li&gt;No safety net: Refactors can silently change how the AI behaves.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The Solution: Promptly
&lt;/h3&gt;

&lt;p&gt;Promptly lets you define AI prompts as ERB or Liquid templates, stored in a Rails-native directory structure. Just like you’d render a view, you render a prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/prompts/welcome_email.erb&lt;/span&gt;
&lt;span class="no"&gt;Hello&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @user.name %&amp;gt;, welcome to our service!
We’re excited to have you join.

# In your mailer or service:
render_prompt("welcome_email", user: @user)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This unlocks the same benefits Rails developers already rely on elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt; → All prompts live in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localization&lt;/strong&gt; → Templates can use I18n just like views.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability&lt;/strong&gt; → You can write RSpec tests that assert on rendered prompt output.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why Rails Conventions Matter
&lt;/h3&gt;

&lt;p&gt;Rails has always thrived on convention over configuration. Promptly doesn’t reinvent the wheel; it extends familiar concepts (views, templates, helpers) into the AI space.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of partials, you have prompt templates.&lt;/li&gt;
&lt;li&gt;Instead of locals, you have prompt variables.&lt;/li&gt;
&lt;li&gt;Instead of ad-hoc strings, you have structured, versionable files.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add it to your Gemfile:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'promptly'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2.Create your first template in app/prompts.&lt;br&gt;
3.Render it anywhere with render_prompt.&lt;br&gt;
4.Add tests to verify the output.&lt;/p&gt;

&lt;p&gt;Full docs are in the &lt;a href="https://github.com/wilburhimself/promptly" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Looking Ahead
&lt;/h3&gt;

&lt;p&gt;Promptly is intentionally small. It’s not trying to be a full-blown AI orchestration platform. But by solving one narrow pain point—making prompts maintainable, testable, and Rails-friendly—it can help Rails apps adopt AI without chaos.&lt;/p&gt;

&lt;p&gt;The next steps may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompt versioning.&lt;/li&gt;
&lt;li&gt;Preview workflows in Rails consoles.&lt;/li&gt;
&lt;li&gt;Deeper integration with related gems like semantic search adapters.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you’re a Rails developer experimenting with AI, try &lt;a href="https://github.com/wilburhimself/promptly" rel="noopener noreferrer"&gt;Promptly&lt;/a&gt;. It may save you from prompt spaghetti before it starts.&lt;/p&gt;

&lt;p&gt;Feedback and contributions are welcome, the best ideas usually come from real-world use.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>🚨 Introducing GemGuard: Automated Security for Ruby Gems (Scan, SBOM, Typosquat, Auto-Fix)</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Mon, 11 Aug 2025 02:05:38 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/introducing-gemguard-automated-security-for-ruby-gems-scan-sbom-typosquat-auto-fix-2p2g</link>
      <guid>https://dev.to/travelingwilbur/introducing-gemguard-automated-security-for-ruby-gems-scan-sbom-typosquat-auto-fix-2p2g</guid>
      <description>&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/wilburhimself/gem_guard" rel="noopener noreferrer"&gt;github.com/wilburhimself/gem_guard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RubyGems: &lt;a href="https://rubygems.org/gems/gem_guard" rel="noopener noreferrer"&gt;rubygems.org/gems/gem_guard&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Scan dependencies for known vulnerabilities (OSV.dev + Ruby Advisory DB)
&lt;/li&gt;
&lt;li&gt;🕵️ Detect typosquat packages before they bite
&lt;/li&gt;
&lt;li&gt;📜 Generate SPDX / CycloneDX SBOMs
&lt;/li&gt;
&lt;li&gt;🛠 Auto-fix vulnerable gems safely
&lt;/li&gt;
&lt;li&gt;⚡ Clean CLI + CI-ready
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version:&lt;/strong&gt; 1.1.x&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why GemGuard?
&lt;/h2&gt;

&lt;p&gt;Because security shouldn’t be an afterthought. It should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pragmatic&lt;/strong&gt; – only what matters, no noise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; – instant feedback in dev or CI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrated&lt;/strong&gt; – works with your normal Ruby workflow&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What is GemGuard?
&lt;/h2&gt;

&lt;p&gt;GemGuard is a lightweight Ruby security tool that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scans your &lt;code&gt;Gemfile.lock&lt;/code&gt; for known vulnerabilities&lt;/li&gt;
&lt;li&gt;Detects typosquat risks via fuzzy matching&lt;/li&gt;
&lt;li&gt;Generates SBOMs (SPDX and CycloneDX)&lt;/li&gt;
&lt;li&gt;Auto-fixes vulnerable gems with safe version upgrades&lt;/li&gt;
&lt;li&gt;Plays nicely with CI/CD&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Installation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add to your Gemfile (recommended for projects)&lt;/span&gt;
gem &lt;span class="s2"&gt;"gem_guard"&lt;/span&gt;, &lt;span class="s2"&gt;"~&amp;gt; 1.1"&lt;/span&gt;

&lt;span class="c"&gt;# Or install globally&lt;/span&gt;
gem &lt;span class="nb"&gt;install &lt;/span&gt;gem_guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Verify:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard version
&lt;span class="c"&gt;# =&amp;gt; 1.1.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Quick Start
&lt;/h4&gt;

&lt;p&gt;Scan your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard scan
&lt;span class="c"&gt;# ✅ No vulnerabilities found!&lt;/span&gt;
&lt;span class="c"&gt;# or exits non‑zero if issues are found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Detect typosquats:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard typosquat
&lt;span class="c"&gt;# No potential typosquat dependencies found.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Generate an SBOM:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard sbom &lt;span class="nt"&gt;--format&lt;/span&gt; spdx &lt;span class="nt"&gt;--output&lt;/span&gt; sbom.spdx.json
gem_guard sbom &lt;span class="nt"&gt;--format&lt;/span&gt; cyclonedx &lt;span class="nt"&gt;--output&lt;/span&gt; bom.cdx.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Auto‑Fix Vulnerabilities
&lt;/h4&gt;

&lt;p&gt;Preview (dry run):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard fix &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Apply fixes (creates a Gemfile.lock backup by default):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard fix
&lt;span class="c"&gt;# 📦 Created backup: Gemfile.lock.backup.2025...&lt;/span&gt;
&lt;span class="c"&gt;# ✅ Updated nokogiri to 1.18.9&lt;/span&gt;
&lt;span class="c"&gt;# 🔄 Running bundle install to update lockfile...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--interactive&lt;/code&gt;: confirm each update&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-backup&lt;/code&gt;: skip lockfile backup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--gemfile&lt;/code&gt;, --lockfile: custom paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tip: Re-scan after fixing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Clean CLI
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="c"&gt;# config, scan, typosquat, sbom, fix, version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Exit codes:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;0: success / no vulns&lt;/li&gt;
&lt;li&gt;1: vulnerabilities found&lt;/li&gt;
&lt;li&gt;2: errors (e.g., missing files)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CI/CD Integration (GitHub Actions)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-scan&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gemguard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.3'&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install gem_guard&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem_guard scan --format json &amp;gt; gemguard-report.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem_guard typosquat --format json &amp;gt; typosquat-report.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload reports&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemguard-reports&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;gemguard-report.json&lt;/span&gt;
            &lt;span class="s"&gt;typosquat-report.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail builds on vulnerabilities (default behavior). If you want non-blocking scans (e.g., on main), wrap with || true or use matrix strategies.&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuration
&lt;/h4&gt;

&lt;p&gt;Create .gemguard.yml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lockfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gemfile.lock&lt;/span&gt;
&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;table&lt;/span&gt;   &lt;span class="c1"&gt;# table | json&lt;/span&gt;
&lt;span class="na"&gt;typosquat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;similarity_threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.82&lt;/span&gt;
  &lt;span class="na"&gt;risk_levels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.9&lt;/span&gt;
    &lt;span class="na"&gt;medium&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View current config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem_guard config &lt;span class="nt"&gt;--show&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why GemGuard?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Minimal setup, zero noise&lt;/li&gt;
&lt;li&gt;Pragmatic defaults, sensible exit codes&lt;/li&gt;
&lt;li&gt;Works offline for typosquat via fallback popular gems&lt;/li&gt;
&lt;li&gt;Well-tested (RSpec), standardrb formatting&lt;/li&gt;
&lt;li&gt;Designed for CI from day 1&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How It Compares
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bundler Audit: great for advisories; GemGuard adds typosquat + SBOM + auto-fix&lt;/li&gt;
&lt;li&gt;OSV-Scanner: broad ecosystem; GemGuard is Ruby-first with tighter UX and auto-fix&lt;/li&gt;
&lt;li&gt;Trivy/Grype: container focus; GemGuard slots into pure-Ruby pipelines easily&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use GemGuard standalone or alongside your existing stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roadmap
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Enriched advisories (GHSA/CVE links, CVSS)&lt;/li&gt;
&lt;li&gt;Optional dependency graph visualizations&lt;/li&gt;
&lt;li&gt;Interactive TUI&lt;/li&gt;
&lt;li&gt;More fix strategies and guards&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Contribute / Feedback
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Issues/PRs welcome: add tests, keep it minimal and intention-revealing&lt;/li&gt;
&lt;li&gt;Prefer failing test → minimal fix → refactor&lt;/li&gt;
&lt;li&gt;Security disclosures: see SECURITY.md&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Try It Now
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;gem_guard
gem_guard scan
gem_guard typosquat
gem_guard fix &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this helps you ship safer Ruby apps with less fuss, drop a ❤️ and share!&lt;/p&gt;

&lt;p&gt;— Built for Rubyists who like fast feedback, clean CLIs, and reliable automation.&lt;/p&gt;

&lt;p&gt;Issues and PRs welcome → &lt;a href="https://github.com/wilburhimself/gem_guard" rel="noopener noreferrer"&gt;github.com/wilburhimself/gem_guard&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Built a RAG System in Rails Using Nomic Embeddings and OpenAI</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Fri, 18 Jul 2025 19:48:41 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/how-i-built-a-rag-system-in-rails-using-nomic-embeddings-and-openai-154e</link>
      <guid>https://dev.to/travelingwilbur/how-i-built-a-rag-system-in-rails-using-nomic-embeddings-and-openai-154e</guid>
      <description>&lt;p&gt;Retrieval-Augmented Generation (RAG) lets you bring your own data to LLMs—and get real answers. I’ll show how I used the open-source nomic-embed-text-v2-moe model for semantic search in a Rails app, while still using OpenAI for generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 What is RAG?
&lt;/h3&gt;

&lt;p&gt;RAG (Retrieval-Augmented Generation) enhances LLMs by feeding them relevant chunks of your data before generating a response. Instead of fine-tuning, we give the model useful context.&lt;/p&gt;

&lt;p&gt;Here's the basic pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;User&lt;/span&gt; &lt;span class="nt"&gt;Question&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;Embed&lt;/span&gt; &lt;span class="nt"&gt;the&lt;/span&gt; &lt;span class="nt"&gt;Question&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;Nomic&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;Vector&lt;/span&gt; &lt;span class="nt"&gt;Search&lt;/span&gt; &lt;span class="nt"&gt;in&lt;/span&gt; &lt;span class="nt"&gt;PgVector&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;Retrieve&lt;/span&gt; &lt;span class="nt"&gt;Relevant&lt;/span&gt; &lt;span class="nt"&gt;Chunks&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;Assemble&lt;/span&gt; &lt;span class="nt"&gt;Prompt&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;Generate&lt;/span&gt; &lt;span class="nt"&gt;Answer&lt;/span&gt; &lt;span class="nt"&gt;with&lt;/span&gt; &lt;span class="nt"&gt;OpenAI&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🧰 The Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rails&lt;/strong&gt; – Backend framework, routes, controllers, and persistence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nomic&lt;/strong&gt; Embedding Model – For semantic understanding of data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; – Lightweight Python server to serve embeddings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PgVector&lt;/strong&gt; – PostgreSQL extension to store and query vector data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI GPT-4 / GPT-3.5&lt;/strong&gt; – For the final response generation&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🛠 Step 1: Run the Nomic Model Locally (Optional but Fast)
&lt;/h3&gt;

&lt;p&gt;You can run the &lt;a href="https://huggingface.co/nomic-ai/nomic-embed-text-v2-moe" rel="noopener noreferrer"&gt;nomic-embed-text-v2-moe&lt;/a&gt; model using &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers&lt;/a&gt; in a Python FastAPI app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-ai/nomic-embed-text-v2-moe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/embed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;input_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes your internal embedding API, replacing OpenAI’s &lt;code&gt;/embeddings&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📄 Step 2: Chunk and Store Your Data
&lt;/h3&gt;

&lt;p&gt;Split your content into short passages (~100–300 words), embed them via your FastAPI endpoint, and store the results in PostgreSQL with &lt;code&gt;pgvector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Add a vector column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-d&lt;/span&gt; your_db &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"CREATE EXTENSION IF NOT EXISTS vector;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddEmbeddingToDocuments&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt; &lt;span class="c1"&gt;# Nomic v2-moe size&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🤖 Step 3: Embed User Queries via Nomic
&lt;/h3&gt;

&lt;p&gt;In your Rails controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Faraday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8000/embed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;input: &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"embedding"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the same model for both document and query embeddings.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔍 Step 4: Perform Vector Search with PgVector
&lt;/h3&gt;

&lt;p&gt;Search your documents for the closest matches using cosine distance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"embedding &amp;lt;-&amp;gt; cube(array[?])"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These top chunks become the context for the LLM.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧾 Step 5: Build a Smart Prompt for OpenAI
&lt;/h3&gt;

&lt;p&gt;Concatenate the top passages and feed them into OpenAI’s chat API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;parameters: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;messages: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s2"&gt;"You are an assistant answering based on the provided context."&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;build_contextual_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  ✅ Why Use Nomic for Embeddings?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;High-quality, open-source, multilingual&lt;/li&gt;
&lt;li&gt;No token limits — runs locally or self-hosted&lt;/li&gt;
&lt;li&gt;Zero vendor lock-in at the embedding layer&lt;/li&gt;
&lt;li&gt;Great performance on MTEB and real-world retrieval&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  💡 Why I Still Use OpenAI for the LLM
&lt;/h3&gt;

&lt;p&gt;The generation step is where OpenAI shines. Instead of replacing it prematurely, I decoupled the embedding stage. Now I can experiment, optimize, and even switch LLMs later if needed.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧠 Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;RAG doesn’t need to be a heavyweight system.&lt;/li&gt;
&lt;li&gt;Open-source embeddings + OpenAI generation = powerful, flexible hybrid.&lt;/li&gt;
&lt;li&gt;PgVector + Rails makes vector search feel native and hackable.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>openai</category>
      <category>ai</category>
      <category>rails</category>
    </item>
    <item>
      <title>The Art of Idiomatic Ruby: Principles and Practices for Elegant Code</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Mon, 17 Mar 2025 17:29:02 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/the-art-of-idiomatic-ruby-principles-and-practices-for-elegant-code-53ej</link>
      <guid>https://dev.to/travelingwilbur/the-art-of-idiomatic-ruby-principles-and-practices-for-elegant-code-53ej</guid>
      <description>&lt;p&gt;As developers, we're constantly seeking to improve our craft and develop a coding style that's both effective and elegant. Ruby stands out among programming languages for its expressiveness and focus on developer happiness. The philosophy behind Ruby and its most popular framework, Rails, has shaped how countless developers think about software design, readability, and the overall development experience.&lt;/p&gt;

&lt;p&gt;In this post, I'll explore the key principles behind idiomatic Ruby, analyze what makes Ruby code truly "Rubyesque," and share practical ways to incorporate these principles into your own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Philosophy of Idiomatic Ruby
&lt;/h2&gt;

&lt;p&gt;Ruby's approach to programming is built on several foundational principles:&lt;/p&gt;

&lt;h3&gt;
  
  
  Convention over Configuration
&lt;/h3&gt;

&lt;p&gt;One of Ruby's most influential frameworks popularized the concept of sensible defaults and conventions. Rather than requiring developers to make countless trivial decisions, idiomatic Ruby emphasizes having established patterns that make assumptions about what developers need and provide streamlined pathways for common tasks.&lt;/p&gt;

&lt;p&gt;This philosophy extends beyond frameworks into coding style, where established patterns and clear conventions are favored over reinventing the wheel or introducing unnecessary complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beautiful Code
&lt;/h3&gt;

&lt;p&gt;In the Ruby community, code isn't just functional—it's a form of craft. There's a strong emphasis on the aesthetics of code, with the understanding that beautiful code is more maintainable, more enjoyable to work with, and ultimately more effective. This focus on beauty manifests in a preference for expressive, readable syntax that almost reads like natural language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Programmer Happiness
&lt;/h3&gt;

&lt;p&gt;"Optimize for programmer happiness" is a consistent theme in the Ruby community. While some languages and frameworks optimize primarily for performance or flexibility, Ruby prioritizes the developer experience. This perspective views coding as a creative act that should be enjoyable rather than tedious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opinionated Software Design
&lt;/h3&gt;

&lt;p&gt;Idiomatic Ruby doesn't shy away from having strong opinions about how software should be built. Rather than trying to accommodate every possible approach, Ruby and its ecosystem often present "The Ruby Way" of doing things. Making strong choices about architecture and design patterns creates a more cohesive and understandable codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elements of Idiomatic Ruby Style
&lt;/h2&gt;

&lt;p&gt;When examining truly elegant Ruby code, several distinctive characteristics emerge:&lt;/p&gt;

&lt;h3&gt;
  
  
  Expressive Naming
&lt;/h3&gt;

&lt;p&gt;Idiomatic Ruby places tremendous importance on choosing the right names for variables, methods, and classes. It favors descriptive, sometimes longer names that clearly communicate intent over terse abbreviations. The goal is code that can be read and understood without extensive comments or documentation.&lt;/p&gt;

&lt;p&gt;For example, instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calc_ttl_prc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Calculate total price with discount&lt;/span&gt;
  &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Idiomatic Ruby prefers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;total_price_with_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Embracing Ruby's Natural Expressiveness
&lt;/h3&gt;

&lt;p&gt;Truly Rubyesque code leans heavily into the language's syntax to create code that feels natural and reads almost like English prose. It makes full use of Ruby's blocks, optional parentheses, and symbol shortcuts to create concise yet readable code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Less idiomatic&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribed?&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_newsletter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# More idiomatic &lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:subscribed?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:send_newsletter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Domain-Driven Design
&lt;/h3&gt;

&lt;p&gt;Idiomatic Ruby often designs code around domain concepts rather than technical implementation details. Classes and methods reflect the business domain, making the code not just functional but a representation of the problem space itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pragmatic Testing Approach
&lt;/h3&gt;

&lt;p&gt;The Ruby community has evolved a pragmatic approach to testing. While testing is valued, there's a focus on testing the outcomes that matter rather than implementation details. System tests that verify functionality from the user's perspective are often favored alongside focused unit tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategic Metaprogramming
&lt;/h3&gt;

&lt;p&gt;Ruby's powerful metaprogramming capabilities are a double-edged sword. Idiomatic Ruby uses metaprogramming strategically to eliminate boilerplate and create more elegant APIs, not as a showcase of technical wizardry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Design Patterns in Ruby
&lt;/h2&gt;

&lt;p&gt;Several design patterns are central to idiomatic Ruby programming:&lt;/p&gt;

&lt;h3&gt;
  
  
  Active Record Pattern
&lt;/h3&gt;

&lt;p&gt;The Active Record pattern, which blends database access and business logic into a single object, is widely used in the Ruby ecosystem. While this approach breaks with strict separation of concerns, it creates an intuitive model that's easy to work with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:reviews&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:category&lt;/span&gt;

  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;numericality: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;greater_than: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;discounted_price&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Modules and Concerns for Shared Behavior
&lt;/h3&gt;

&lt;p&gt;Instead of deep inheritance hierarchies, idiomatic Ruby favors using modules as "Concerns" to share behavior across multiple classes. This creates more flexible composition without the rigid structures that deep inheritance can create.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Trackable&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:audit_logs&lt;/span&gt;
    &lt;span class="n"&gt;after_save&lt;/span&gt; &lt;span class="ss"&gt;:record_change&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_change&lt;/span&gt;
    &lt;span class="n"&gt;audit_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"updated"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Trackable&lt;/span&gt;
  &lt;span class="c1"&gt;# Product-specific code...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Convention-Based Routing
&lt;/h3&gt;

&lt;p&gt;RESTful routing establishes clear conventions for mapping HTTP verbs and URLs to controller actions, creating a predictable structure for web applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Objects for Complex Operations
&lt;/h3&gt;

&lt;p&gt;For operations that span multiple models or incorporate complex business logic, Ruby developers often employ service objects—dedicated classes that encapsulate a single operation or transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProcessor&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="vi"&gt;@payment_details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payment_details&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;valid?&lt;/span&gt;

    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;process_payment&lt;/span&gt;
      &lt;span class="n"&gt;update_inventory&lt;/span&gt;
      &lt;span class="n"&gt;send_confirmation&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Private methods for each step...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Write Idiomatic Ruby
&lt;/h2&gt;

&lt;p&gt;If you're inspired by Ruby's elegant approach and want to incorporate elements of idiomatic style into your own work, here are some practical steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prioritize Readability Above All
&lt;/h3&gt;

&lt;p&gt;Write code for humans first, computers second. Invest time in creating clear, expressive code that future developers (including your future self) will easily understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Embrace Ruby's Unique Features
&lt;/h3&gt;

&lt;p&gt;Work with Ruby's strengths rather than fighting against them. Learn what makes Ruby special—blocks, procs, symbols, method_missing, etc.—and use those features to create more elegant code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Instead of:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"banana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;fruit&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fruit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Write:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"banana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:upcase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Design Around Your Domain
&lt;/h3&gt;

&lt;p&gt;Structure your code to reflect the business or problem domain rather than technical considerations. Your classes and methods should speak the language of your users and stakeholders.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Follow Conventions but Break Them When Necessary
&lt;/h3&gt;

&lt;p&gt;Establish and follow conventions in your codebase, but be willing to break them when they don't serve your needs. Ruby isn't dogmatic about rules—it's pragmatic about results.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Value Simplicity Over Complexity
&lt;/h3&gt;

&lt;p&gt;Resist the urge to over-engineer. The Ruby community often advocates for the simplest solution that works, even if it's not the most theoretically pure or technically impressive.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Consider the Whole System
&lt;/h3&gt;

&lt;p&gt;Think holistically about your application rather than optimizing individual components in isolation. An idiomatic approach considers how all parts of a system work together to create a cohesive whole.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Make Development Enjoyable
&lt;/h3&gt;

&lt;p&gt;Finally, remember that coding should be enjoyable. Create tools, processes, and code that make development a pleasure rather than a chore—this is perhaps the most Ruby-like approach of all.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Pay Attention to Code Smells
&lt;/h3&gt;

&lt;p&gt;Learn to recognize and refactor common code smells:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Code smell: Long method&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_order&lt;/span&gt;
  &lt;span class="c1"&gt;# 100 lines of code doing many different things&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Refactored: Extracted methods with clear purposes&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_order&lt;/span&gt;
  &lt;span class="n"&gt;validate_order&lt;/span&gt;
  &lt;span class="n"&gt;process_payment&lt;/span&gt;
  &lt;span class="n"&gt;update_inventory&lt;/span&gt;
  &lt;span class="n"&gt;send_confirmation&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  9. Follow Community Style Guides
&lt;/h3&gt;

&lt;p&gt;Familiarize yourself with established Ruby style guides like the one from GitHub or Rubocop's defaults. These encapsulate years of community wisdom about what makes Ruby code clear and maintainable.&lt;/p&gt;




&lt;p&gt;Idiomatic Ruby represents a philosophy of software development that values human factors alongside technical considerations—code that's not just functional but beautiful, maintainable, and enjoyable to work with.&lt;/p&gt;

&lt;p&gt;By prioritizing readability, embracing Ruby's unique features, and focusing on developer happiness, you can create code that not only works but brings joy to those who interact with it. That's the true spirit of Ruby.&lt;/p&gt;

&lt;p&gt;What aspects of Ruby's style have influenced your own development approach? What patterns or practices do you find most valuable in your own Ruby projects? Share your thoughts and experiences in the comments below.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>designpatterns</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Understanding Form Objects in Ruby on Rails</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Thu, 13 Mar 2025 21:38:04 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/understanding-form-objects-in-ruby-on-rails-3bf3</link>
      <guid>https://dev.to/travelingwilbur/understanding-form-objects-in-ruby-on-rails-3bf3</guid>
      <description>&lt;p&gt;In Ruby on Rails applications, handling complex forms can become messy when too much logic is stuffed into models or controllers. This is where &lt;strong&gt;Form Objects&lt;/strong&gt; come in—a design pattern that helps separate form-related logic from Active Record models, making our applications more maintainable and testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Traditional Forms
&lt;/h2&gt;

&lt;p&gt;By default, Rails encourages using Active Record models directly in forms. However, this approach has some downsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fat Models&lt;/strong&gt;: Business logic and validation bloat the model, making it harder to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fat Controllers&lt;/strong&gt;: Controllers become responsible for handling complex form submissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Models in One Form&lt;/strong&gt;: Standard Rails forms work well with single models, but handling multiple related models can be cumbersome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To address these issues, we use &lt;strong&gt;Form Objects&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Form Object?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Form Object&lt;/strong&gt; is a Plain Old Ruby Object (PORO) designed to handle form submissions. It encapsulates form-specific validations and persistence logic while keeping models and controllers clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Use a Form Object
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;When a form involves multiple models.&lt;/li&gt;
&lt;li&gt;When you want to keep your controllers thin and models focused.&lt;/li&gt;
&lt;li&gt;When form validation differs from the database schema.&lt;/li&gt;
&lt;li&gt;When reusing form logic across multiple places in your application.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementing a Form Object in Rails
&lt;/h2&gt;

&lt;p&gt;Let's walk through an example where we create a &lt;code&gt;UserRegistrationForm&lt;/code&gt; that handles user sign-ups along with profile information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Form Object
&lt;/h3&gt;

&lt;p&gt;Create a new file in &lt;code&gt;app/forms/user_registration_form.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRegistrationForm&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;

  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bio&lt;/span&gt;

  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;confirmation: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MailTo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EMAIL_REGEXP&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;valid?&lt;/span&gt;

    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;password: &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;bio: &lt;/span&gt;&lt;span class="n"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordInvalid&lt;/span&gt;
    &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Use the Form Object in the Controller
&lt;/h3&gt;

&lt;p&gt;Modify the &lt;code&gt;UsersController&lt;/code&gt; to use the &lt;code&gt;UserRegistrationForm&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UserRegistrationForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UserRegistrationForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s1"&gt;'User registered successfully!'&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_params&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user_registration_form&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:bio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Update the View
&lt;/h3&gt;

&lt;p&gt;Modify the &lt;code&gt;new.html.erb&lt;/code&gt; view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;users_path&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_field&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;password_field&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;password_field&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:bio&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_area&lt;/span&gt; &lt;span class="ss"&gt;:bio&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="s1"&gt;'Register'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benefits of Using Form Objects
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separation of Concerns&lt;/strong&gt;: Keeps models and controllers focused on their primary responsibilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Testability&lt;/strong&gt;: You can test form logic independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier Maintenance&lt;/strong&gt;: Avoids bloated models with unnecessary validations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Form Objects in Ruby on Rails provide a clean way to manage complex form submissions while keeping code organized. By encapsulating form logic in a dedicated class, we improve maintainability, readability, and re-usability in our applications.&lt;/p&gt;

&lt;p&gt;If you're dealing with bloated models and controllers due to complex forms, consider adopting Form Objects as a structured solution!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Background Jobs in Rails: A Look at SolidQueue</title>
      <dc:creator>Wilbur Suero</dc:creator>
      <pubDate>Wed, 12 Mar 2025 05:43:03 +0000</pubDate>
      <link>https://dev.to/travelingwilbur/background-jobs-in-rails-a-look-at-solidqueue-21nh</link>
      <guid>https://dev.to/travelingwilbur/background-jobs-in-rails-a-look-at-solidqueue-21nh</guid>
      <description>&lt;p&gt;Coming from a Rails background, one of the most common patterns developers rely on is background job processing. Traditionally, this has meant reaching for Sidekiq, Resque, or Delayed Job—external tools that require Redis or additional infrastructure. But what if you could handle background jobs natively within your database? Enter SolidQueue.&lt;/p&gt;

&lt;p&gt;In this post, I’ll explore how SolidQueue changes the game for Rails developers, how it compares to existing background job solutions, and how you can start using it today.&lt;/p&gt;

&lt;h2&gt;
  
  
  SolidQueue in Rails 8: Built-In Background Jobs
&lt;/h2&gt;

&lt;p&gt;With the release of Rails 8, SolidQueue is now bundled as the default background job system. This means that if you're starting a new Rails 8 application, you already have everything you need to manage background jobs without any additional dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimum Required Version
&lt;/h3&gt;

&lt;p&gt;SolidQueue requires Rails 7.1 or later to function. However, for full integration and the best experience, Rails 8 is recommended since it comes pre-configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding SolidQueue: Background Jobs Without External Dependencies
&lt;/h2&gt;

&lt;p&gt;SolidQueue is a database-backed queuing system designed to work seamlessly with Active Job in Rails. Unlike Sidekiq or Resque, which rely on Redis, SolidQueue keeps everything within your application’s SQL database. The result? Simplified infrastructure, reduced operational complexity, and one less dependency to worry about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features of SolidQueue
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Native Database Integration&lt;/strong&gt;: Jobs are stored and managed using PostgreSQL, MySQL, or SQLite—whichever database your Rails app already uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active Job Compatibility&lt;/strong&gt;: Works out of the box with Rails' Active Job framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Additional Services Required&lt;/strong&gt;: Eliminates the need for Redis or external queueing systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mission Control UI&lt;/strong&gt;: Provides a web interface to monitor and manage jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Job Retries &amp;amp; Error Handling&lt;/strong&gt;: Ensures reliable job execution.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to Set Up SolidQueue in Rails 8
&lt;/h2&gt;

&lt;p&gt;If you're using Rails 8, SolidQueue is already included, and you just need to enable it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling SolidQueue in a New Rails 8 App
&lt;/h3&gt;

&lt;p&gt;For new Rails 8 applications, SolidQueue is the default Active Job adapter. To ensure it is enabled, check your &lt;code&gt;config/application.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:solid_queue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, run the database migrations to set up the necessary tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting Up SolidQueue in an Existing Rails 7.1+ App
&lt;/h3&gt;

&lt;p&gt;If you're on Rails 7.1 or later and want to use SolidQueue, you'll need to install it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add to your Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'solid_queue'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, run the installation generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails generate solid_queue:install
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Defining and Enqueueing Jobs
&lt;/h2&gt;

&lt;p&gt;Creating jobs with SolidQueue is no different from using Active Job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExampleJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Processing job for &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;ExampleJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running Jobs
&lt;/h3&gt;

&lt;p&gt;SolidQueue requires a supervisor process to run jobs. Start it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;solid_queue:start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will process jobs from the database queue, similar to how Sidekiq works.&lt;/p&gt;

&lt;h2&gt;
  
  
  A More Detailed Example: Processing an Order in Rails 8
&lt;/h2&gt;

&lt;p&gt;Let’s say we’re building an e-commerce application and need to process orders asynchronously. With SolidQueue in Rails 8, we can handle this with ease.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Job
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrderJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_payment!&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Order #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has been processed."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Enqueue the Job When an Order is Created
&lt;/h3&gt;

&lt;p&gt;Modify your &lt;code&gt;OrdersController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="vi"&gt;@order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Your order is being processed."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Start the Worker Process
&lt;/h3&gt;

&lt;p&gt;To start processing jobs, simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;solid_queue:start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, every time a new order is created, it will be processed in the background by SolidQueue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing SolidQueue to Sidekiq
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SolidQueue (Database-Backed)&lt;/th&gt;
&lt;th&gt;Sidekiq (Redis-Backed)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependency&lt;/td&gt;
&lt;td&gt;Uses SQL database&lt;/td&gt;
&lt;td&gt;Requires Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;No extra services needed&lt;/td&gt;
&lt;td&gt;Needs Redis, Sidekiq&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Good for most applications&lt;/td&gt;
&lt;td&gt;Optimized for high throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI for Jobs&lt;/td&gt;
&lt;td&gt;Mission Control UI&lt;/td&gt;
&lt;td&gt;Sidekiq Web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;Database transactions ensure safety&lt;/td&gt;
&lt;td&gt;Redis-based, requires persistence tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you’re running a high-throughput application with millions of background jobs per day, Sidekiq may still be the best choice. But for most Rails applications, SolidQueue provides a much simpler, integrated solution without the extra operational burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should You Use SolidQueue?
&lt;/h2&gt;

&lt;p&gt;SolidQueue is a great choice if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to avoid the overhead of running Redis.&lt;/li&gt;
&lt;li&gt;Your application already relies heavily on its database.&lt;/li&gt;
&lt;li&gt;You prefer an all-in-one solution with built-in job monitoring.&lt;/li&gt;
&lt;li&gt;You need Active Job compatibility with minimal setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, if you need extreme performance and high concurrency, Sidekiq might still be the better option.&lt;/p&gt;

&lt;p&gt;SolidQueue represents a shift in how Rails developers can handle background jobs. By leveraging the database as a queueing system, it removes the need for external services like Redis while maintaining reliability and simplicity. With Rails 8, SolidQueue is now the default, making background job processing more accessible than ever.&lt;/p&gt;

&lt;p&gt;If you’re using Rails 8, try it out today—it’s already built in! And if you're on an older version, upgrading to Rails 8 or installing SolidQueue manually can help simplify your background job processing.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
