<?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: reinteractive</title>
    <description>The latest articles on DEV Community by reinteractive (@reinteractive).</description>
    <link>https://dev.to/reinteractive</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2712416%2F20c2ebbb-019f-429a-b514-2c9458b3d94e.png</url>
      <title>DEV Community: reinteractive</title>
      <link>https://dev.to/reinteractive</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reinteractive"/>
    <language>en</language>
    <item>
      <title>reinteractive: Shaping the Future of Rails Through Leadership Insights</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Tue, 15 Jul 2025 01:26:41 +0000</pubDate>
      <link>https://dev.to/reinteractive/reinteractive-shaping-the-future-of-rails-through-leadership-insights-1pli</link>
      <guid>https://dev.to/reinteractive/reinteractive-shaping-the-future-of-rails-through-leadership-insights-1pli</guid>
      <description>&lt;p&gt;reinteractive is on a mission to understand and help shape the future trajectory of the Rails industry. We believe that the most valuable insights come directly from those at the forefront of innovation and decision-making.&lt;/p&gt;

&lt;p&gt;To achieve this, reinteractive is currently conducting a crucial survey, inviting CTOs, Founders, and key leaders within the Rails ecosystem to share their perspectives. This isn't just about gathering data; it's about building a comprehensive understanding of where the industry is headed, directly from the minds that are guiding it.&lt;/p&gt;

&lt;p&gt;Participants are invited to engage in a brief 15-minute discussion. All responses are handled with the utmost confidentiality and will be shared anonymously in the collated results. Once these insights are gathered, reinteractive is committed to sharing them publicly, with plans to present the findings at future industry conferences.&lt;/p&gt;

&lt;p&gt;Understanding that leaders are often busy, reinteractive offers flexible scheduling, allowing participants to book a time that suits them, even if their next few weeks are particularly demanding. The survey will remain open for an extended period to ensure the broadest possible range of insights and perspectives are captured.&lt;/p&gt;

&lt;p&gt;reinteractive is dedicated to fostering a collaborative environment where industry leaders can contribute to a collective vision for the future of Rails.&lt;/p&gt;

&lt;p&gt;To contribute your insights, book a time with reinteractive here: &lt;a href="https://calendar.app.google/15kqwmgNfQ9oQDSu9" rel="noopener noreferrer"&gt;https://calendar.app.google/15kqwmgNfQ9oQDSu9&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Introducing SecretLink: The Secure Way to Share Sensitive Information</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Thu, 29 May 2025 03:13:01 +0000</pubDate>
      <link>https://dev.to/reinteractive/introducing-secretlink-the-secure-way-to-share-sensitive-information-34ka</link>
      <guid>https://dev.to/reinteractive/introducing-secretlink-the-secure-way-to-share-sensitive-information-34ka</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/secretlink-secure-sharing-sensitive-information" rel="noopener noreferrer"&gt;Errol Schmidt&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;IN A NUTSHELL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SecretLink offers essential, free, open-source security for sharing sensitive development data like API keys.&lt;/p&gt;

&lt;p&gt;SecretLink’s open-source model ensures transparency and trust, a key factor for modern tech teams.&lt;/p&gt;

&lt;p&gt;Use SecretLink to experience simple, self-destructing data transfer, eliminating insecure sharing via email or chat&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;One of the tools we utilise at reinteractive for our own development projects is &lt;a href="https://secretlink.org/" rel="noopener noreferrer"&gt;Secretlink&lt;/a&gt;. It is a tool that allows our clients and developers to securely share project information that requires high levels of security. This includes environment variables, API keys, user passwords etc.&lt;/p&gt;

&lt;p&gt;We developed and maintain &lt;a href="https://secretlink.org/" rel="noopener noreferrer"&gt;Secretlink&lt;/a&gt; to avoid passing around sensitive information using emails or Slack.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SecretLink Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://secretlink.org/" rel="noopener noreferrer"&gt;Secretlink&lt;/a&gt; encrypts your secrets and only the recipient of the secret is provided the key to decrypt. &lt;a href="https://secretlink.org/" rel="noopener noreferrer"&gt;Secretlink&lt;/a&gt; does not store the decryption key making the data secure. This is a one-way cipher.&lt;/p&gt;

&lt;p&gt;As soon as the secret has been opened by the recipient the encrypted data is removed from the database and cannot be accessed by anyone again.&lt;/p&gt;

&lt;p&gt;This allows you to send any form of sensitive information without the risk of it being accessed by any other party.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who can use Secretlink
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://secretlink.org/" rel="noopener noreferrer"&gt;Secretlink&lt;/a&gt; is available to anyone as a free service. All you need to do is enter your email address and you will be sent a token to set up your secret and enter the recipient.&lt;/p&gt;

&lt;p&gt;Feel free to share &lt;a href="https://secretlink.org/" rel="noopener noreferrer"&gt;Secretlink&lt;/a&gt; and make it available to anyone within your team or network.&lt;/p&gt;

&lt;p&gt;We have a number of our clients who use Secretlink for the transfer of secrets internally with their own team. It is a much better solution than using emails which can accidentally be forwarded, exposing secrets to the wrong recipients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://secretlink.org/" rel="noopener noreferrer"&gt;Secretlink&lt;/a&gt; is an Open Source project under a GPL licence. It is written in Ruby on Rails and actively maintained to the latest rails versions. Any developer can &lt;a href="https://github.com/reinteractive/secretlink" rel="noopener noreferrer"&gt;fork the code&lt;/a&gt; and utilise it for their own purposes. We only ask that you don’t use it for commercial or competitive purposes, but use it for your own clients and staff.&lt;/p&gt;

&lt;p&gt;Feel free to make any recommendations for improvements of the &lt;a href="https://github.com/reinteractive/secretlink" rel="noopener noreferrer"&gt;code&lt;/a&gt;, through raising an issue or a pull request.&lt;/p&gt;

&lt;p&gt;Setting up the app locally is relatively easy to do. All of the instructions are covered within the &lt;a href="https://github.com/reinteractive/secretlink" rel="noopener noreferrer"&gt;Readme.md file&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The repo can be accessed via this link &lt;a href="https://dev.togithub/reinteractive/secretlink"&gt;github/reinteractive/secretlink&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We wrote Secretlink for our own purposes, and are happy for it to be used by the Rails community. Please add your enhancements and create any pull requests you think will make the service better.&lt;/p&gt;

</description>
      <category>security</category>
      <category>softwaredevelopment</category>
      <category>development</category>
      <category>github</category>
    </item>
    <item>
      <title>Rails Action Mailer: Rendering Charts or Graphs in your Email</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Tue, 29 Apr 2025 11:07:58 +0000</pubDate>
      <link>https://dev.to/reinteractive/rails-action-mailer-rendering-charts-or-graphs-in-your-email-2452</link>
      <guid>https://dev.to/reinteractive/rails-action-mailer-rendering-charts-or-graphs-in-your-email-2452</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/rails-action-mailer-embed-charts-graphs-quickchart" rel="noopener noreferrer"&gt;Charles Martinez&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recently, I had to look into a few ways to embed a chart into Rails mailer views. Most of the time, I just use chartkick because its simple and easy to use. But in mailers, Chartkick can’t be used directly, so you have to embed an image of the chart for it to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating Chart Images
&lt;/h2&gt;

&lt;p&gt;After a while, I bumped into QuickChart an Open Source library to generate chart images by just generating the url with the correct query parameters. And it offers a lot of chart options &lt;a href="https://quickchart.io/gallery/" rel="noopener noreferrer"&gt;https://quickchart.io/gallery/&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://quickchart.io/chart?c={type:'bar',data:{labels:['Q1','Q2','Q3','Q4'], datasets:[{label:'Users',data:[50,60,70,180]},{label:'Revenue',data:[100,200,300,400]}]}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And another thing, there’s also a gem that acts as a Ruby client for QuickChart -&lt;a href="https://github.com/typpo/quickchart-ruby" rel="noopener noreferrer"&gt;https://github.com/typpo/quickchart-ruby&lt;/a&gt;&lt;br&gt;
Given that, you can very simply generate a QuickChart object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;u/chart = QuickChart.new(
  {
    type: 'bar',
    data: {
      labels: ['Q1', 'Q2', 'Q3', 'Q4'],
      datasets: [
        {
          label: 'Users',
          data: [50, 100, 120, 150]
        },
        {
          label: 'Revenue',
          data: [100, 200, 300, 450]
        },
      ]
    },
    options: {
      title: {
        display: true,
        text: 'Users vs Revenue',
        fontSize: 16,
        padding: 16,
      },
    },
  }
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then render the URL of that object in your Mailer View.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# apps/views/sample_mailer/sample_email.html.erb

&amp;lt;img src="&amp;lt;%= @chart.get_url %&amp;gt;"&amp;gt;

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

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2F7lr69lk31k91suw0o1wr.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.amazonaws.com%2Fuploads%2Farticles%2F7lr69lk31k91suw0o1wr.png" alt="Chart" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that’s it! You can now embed and render charts in your Mailer Views. Very simple and quick to implement.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Refinement: The Correct Way To Monkey-Patch in Ruby</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Mon, 28 Apr 2025 03:58:40 +0000</pubDate>
      <link>https://dev.to/reinteractive/refinement-the-correct-way-to-monkey-patch-in-ruby-1icl</link>
      <guid>https://dev.to/reinteractive/refinement-the-correct-way-to-monkey-patch-in-ruby-1icl</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/ruby-refinements-vs-monkey-patching-best-practices" rel="noopener noreferrer"&gt;Allan Andal&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ruby's &lt;a href="https://docs.ruby-lang.org/en/master/Refinement.html" rel="noopener noreferrer"&gt;Refinement&lt;/a&gt; feature emerged as an experimental addition in Ruby 2.0 and became a full-fledged feature starting with Ruby 2.1. It’s a neat way to tweak a class’s methods without messing with how it works everywhere else in your app. Instead of monkey-patching—where you’d change something like &lt;code&gt;String&lt;/code&gt; or &lt;code&gt;Integer&lt;/code&gt; and it impacts your whole program—Refinements let you keep those changes contained to a specific module or class. You activate them when needed with &lt;code&gt;using&lt;/code&gt; keyword. This addresses monkey-patching’s danger of silent—bugs, conflicts, and maintenance woes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Old way
&lt;/h3&gt;

&lt;p&gt;Let's say you want to add a new method that converts a string "Yes" and "No" to a boolean value. All we need to do is reopen the class and add the method:&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;String&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_bool&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;downcase&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="sx"&gt;%w[true yes 1]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="sx"&gt;%w[false no 0]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid boolean string: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;self&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="k"&gt;end&lt;/span&gt;

&lt;span class="s2"&gt;"True"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

&lt;span class="s2"&gt;"FALSE"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy right? However, some problems can arise with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Its everywhere&lt;/strong&gt;. It gets applied to all &lt;code&gt;String&lt;/code&gt; objects in the application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subtle bugs&lt;/strong&gt;: Monkey patches are hard to track. A method added in one file might break logic in another, with no clear trail to debug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Library conflicts&lt;/strong&gt;: Some gems monkey-patch core classes (no need to look far, active_support does it). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance hell&lt;/strong&gt;. Tracking global changes becomes a nightmare when teams of multiple developers patch the same class.
Monkey-patching’s flexibility made it a staple in early Ruby code, but its lack of discipline often turned small tweaks into big problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Using Refinements
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.ruby-lang.org/en/master/Refinement.html" rel="noopener noreferrer"&gt;Refinements&lt;/a&gt; replace monkey-patching by scoping changes to where they’re needed. Instead of polluting &lt;code&gt;String&lt;/code&gt; globally, you define a refinement in a module:&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;BooleanString&lt;/span&gt;
  &lt;span class="n"&gt;refine&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_bool&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;downcase&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="sx"&gt;%w[true yes 1]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="sx"&gt;%w[false no 0]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid boolean string: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;self&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="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Outside the refinement, String is unchanged&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt; &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Not defined yet"&lt;/span&gt;

&lt;span class="c1"&gt;# Activate the refinement&lt;/span&gt;
&lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="no"&gt;BooleanString&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;   &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;     &lt;span class="c1"&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"maybe"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; ArgumentError: Invalid boolean string: maybe&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Compared to the old way, using Refinements offer clear benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scoped Changes&lt;/strong&gt;: Unlike monkey-patching’s global reach, &lt;code&gt;to_bool&lt;/code&gt; exists only where &lt;code&gt;BooleanString&lt;/code&gt; is activated, leaving &lt;code&gt;String&lt;/code&gt; untouched elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Conflicts&lt;/strong&gt;: Refinements avoid clashing with gems or other code, as their effects are isolated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier Debugging&lt;/strong&gt;: If something breaks, you know exactly where the refinement is applied—no hunting through global patches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaner Maintenance&lt;/strong&gt;: Scoping makes it clear who’s using what, simplifying teamwork and long-term upkeep.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Even better approach (Ruby 2.4+, using &lt;code&gt;import_methods&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Since Ruby 2.4, &lt;code&gt;import_methods&lt;/code&gt; lets you pull methods from a module into a refinement, reusing existing code. Suppose you have a &lt;code&gt;BooleanString&lt;/code&gt; module with &lt;code&gt;to_bool&lt;/code&gt; logic:&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;BooleanString&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_bool&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;downcase&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="sx"&gt;%w[true yes 1]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="sx"&gt;%w[false no 0]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid boolean string: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;self&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="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MyContext&lt;/span&gt;
  &lt;span class="n"&gt;refine&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;import_methods&lt;/span&gt; &lt;span class="no"&gt;BooleanString&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Outside the refinement, String is unchanged&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt; &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Not defined yet"&lt;/span&gt;

&lt;span class="c1"&gt;# Activate the refinement&lt;/span&gt;
&lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="no"&gt;MyContext&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;   &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;     &lt;span class="c1"&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"maybe"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bool&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; ArgumentError: Invalid boolean string: maybe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Refinements address the old monkey-patching problems head-on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large Projects&lt;/strong&gt;: Monkey-patching causes chaos in big codebases; Refinements keep changes isolated, reducing team friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Library Safety&lt;/strong&gt;: Unlike global patches that "can" break gems, Refinements stay private, ensuring compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prototyping&lt;/strong&gt;: Refinements offer a sandbox for testing methods, unlike monkey patches that commit you to global changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;a href="https://www.ruby-lang.org/en/news/2024/12/25/ruby-3-4-0-released/" rel="noopener noreferrer"&gt;Ruby 3.4&lt;/a&gt;'s reduced performance overhead makes Refinements a practical replacement, where monkey-patching’s simplicity once held sway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scope Tightly&lt;/strong&gt;: Instead of making blanket changes on classes (specially on based Ruby data types), use only on specific classes or methods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name Clearly&lt;/strong&gt;: This probably is the hardest part (naming things), but pick module names to show intent, avoiding monkey-patching’s ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug Smartly&lt;/strong&gt;: Ruby 3.4’s clearer errors beat tracing global patches—check &lt;code&gt;using&lt;/code&gt; if methods vanish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse Code&lt;/strong&gt;: Use &lt;code&gt;import_methods&lt;/code&gt; to share logic, a step up from monkey-patching’s copy-paste hacks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Whether you’re building new features, dodging library issues, or just playing around with ideas, Refinements are a small change that makes a huge difference. Next time you’re tempted to reopen a class and go wild, give Refinements a shot—you’ll thank yourself later.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Adding an AI chat to your Ruby on Rails application</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Wed, 02 Apr 2025 03:34:00 +0000</pubDate>
      <link>https://dev.to/reinteractive/adding-an-ai-chat-to-your-ruby-on-rails-application-4b3j</link>
      <guid>https://dev.to/reinteractive/adding-an-ai-chat-to-your-ruby-on-rails-application-4b3j</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/rails-ai-chat-integration" rel="noopener noreferrer"&gt;Todd Price&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unless you've been living under a rock for the last couple of years, you've heard about AI and how one day it will do everything for you. Well, we aren't quite at AGI yet but we are certainly on the way. So to better understand our future computer overlords I've spent a lot of time using them and have recently been experimenting with the &lt;a href="https://rubyllm.com/" rel="noopener noreferrer"&gt;RubyLLM Gem&lt;/a&gt;. It's a great gem which makes it very easy to integrate the major LLM providers into your rails app (at the time of writing only Anthropic, DeepSeek, Gemini and OpenAI are supported).&lt;/p&gt;

&lt;p&gt;To demonstrate, I'm going to add an AI chat to a new rails 8 application but you can just as easily apply most of this to your existing rails application. We'll go beyond the most basic setup and allow each user to have their own personal chats with the AI.&lt;/p&gt;

&lt;p&gt;Let's start by setting up the a new app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new ai_chat &lt;span class="nt"&gt;--database&lt;/span&gt; postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then follow &lt;a href="https://reinteractive.com/articles/tutorial-series-new-to-rails/rails8-user-auth-registration" rel="noopener noreferrer"&gt;Suman's post&lt;/a&gt; to use the new built-in rails user auth. Alternatively, use your preferred user &amp;amp; auth setup.&lt;/p&gt;

&lt;p&gt;Now we're ready to add in ruby_llm:&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;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"dotenv"&lt;/span&gt;   &lt;span class="c1"&gt;# for managing API keys, you may want to handle them differently&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"ruby_llm"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add in an initializer to set the API key for your provider(s) of your choice&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/ruby_llm.rb&lt;/span&gt;
&lt;span class="no"&gt;RubyLLM&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;anthropic_api_key&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="s2"&gt;"ANTHROPIC_API_KEY"&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;deepseek_api_key&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="s2"&gt;"DEEPSEEK_API_KEY"&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;gemini_api_key&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="s2"&gt;"GEMINI_API_KEY"&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;openai_api_key&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="s2"&gt;"OPENAI_API_KEY"&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;Set up your &lt;code&gt;.env&lt;/code&gt; file if using dotenv (however you choose to save these keys, keep them secure, don't commit to version control)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPENAI_API_KEY=sk-proj-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we create the new models.&lt;br&gt;
First, we create our &lt;code&gt;Chat&lt;/code&gt; model which will handle the conversation:&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/models/chat.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Chat&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;acts_as_chat&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
  &lt;span class="n"&gt;broadcasts_to&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;chat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"chat_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;chat&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="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;The &lt;code&gt;acts_as_chat&lt;/code&gt; method comes from RubyLLM and provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Message management&lt;/li&gt;
&lt;li&gt;LLM provider integration&lt;/li&gt;
&lt;li&gt;Token tracking&lt;/li&gt;
&lt;li&gt;History management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we create our Message model to handle individual messages in the chat. Each message represents either user input or AI responses:&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/models/message.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&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;acts_as_message&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 &lt;code&gt;acts_as_message&lt;/code&gt; method from RubyLLM provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Role management (user/assistant/system)&lt;/li&gt;
&lt;li&gt;Token counting for both input and output&lt;/li&gt;
&lt;li&gt;Content formatting and sanitization&lt;/li&gt;
&lt;li&gt;Integration with the parent Chat model&lt;/li&gt;
&lt;li&gt;Tool call handling capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, the ToolCall model. I'll cover this in another post, but you need to add it here for RubyLLM to work.&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/models/tool_call.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToolCall&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;acts_as_tool_call&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we link the chats to users:&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/models/user.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&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="c1"&gt;# ...existing code&lt;/span&gt;

  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:chats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;dependent: :destroy&lt;/span&gt;

  &lt;span class="c1"&gt;# ...existing code&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the migrations:&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;# db/migrate/YYYYMMDDHHMMSS_create_chats.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateChats&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;8.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;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:chats&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &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;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:model_id&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;end&lt;/span&gt;


&lt;span class="c1"&gt;# db/migrate/YYYYMMDDHHMMSS_create_messages.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateMessages&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;8.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;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:messages&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &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;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:role&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:model_id&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:input_tokens&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:output_tokens&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:tool_call&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;end&lt;/span&gt;


&lt;span class="c1"&gt;# db/migrate/YYYYMMDDHHMMSS_create_tool_calls.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateToolCalls&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;8.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;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:tool_calls&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &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;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonb&lt;/span&gt; &lt;span class="ss"&gt;:arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:tool_calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tool_call_id&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;Run the migrations:&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;p&gt;Then we'll set up ActionCable so we can stream the chat and make it appear as though the AI is typing out the response.&lt;br&gt;
For further details on this, see the &lt;a href="https://guides.rubyonrails.org/v8.0/action_cable_overview.html" rel="noopener noreferrer"&gt;Rails Guides&lt;/a&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;# app/channels/application_cable/connection.rb&lt;/span&gt;
&lt;span class="c1"&gt;# This file was created by rails g authentication so if you are using a different auth setup you'll need to adapt this&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ApplicationCable&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionCable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Connection&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
    &lt;span class="n"&gt;identified_by&lt;/span&gt; &lt;span class="ss"&gt;:current_user&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;
      &lt;span class="n"&gt;set_current_user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;reject_unauthorized_connection&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;set_current_user&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Session&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;id: &lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:session_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
          &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&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;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="c1"&gt;# app/channels/application_cable/channel.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ApplicationCable&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionCable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Channel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="c1"&gt;# app/channels/chat_channel.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatChannel&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationCable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Channel&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subscribed&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Chat&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;stream_for&lt;/span&gt; &lt;span class="n"&gt;chat&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascipt/channels/consumer.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createConsumer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@rails/actioncable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createConsumer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="c1"&gt;// app/javascipt/channels/chat_channel.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;consumer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./consumer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ChatChannel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chatId&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;Now we set up our controllers.&lt;/p&gt;

&lt;p&gt;First, our &lt;code&gt;ChatsController&lt;/code&gt; which will handle the overall conversation. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index action for listing all user's chats&lt;/li&gt;
&lt;li&gt;Create action for starting new conversations for a user&lt;/li&gt;
&lt;li&gt;Show action for viewing a user's individual chats&lt;/li&gt;
&lt;li&gt;Scoped queries to ensure users can only access their own chats
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/chats_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatsController&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;index&lt;/span&gt;
    &lt;span class="vi"&gt;@chats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat_scope&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;@chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat_scope&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;if&lt;/span&gt; &lt;span class="vi"&gt;@chat&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="vi"&gt;@chat&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;:index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat_scope&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&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;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_scope&lt;/span&gt;
    &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chats&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;Next, we create our &lt;code&gt;MessagesController&lt;/code&gt; to handle individual message creation and the AI response.&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/controllers/messages_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessagesController&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;@chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_chat&lt;/span&gt;

    &lt;span class="no"&gt;GenerateAiResponseJob&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;@chat&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:content&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;@chat&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;find_chat&lt;/span&gt;
    &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chats&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:chat_id&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;message_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;:message&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;:content&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;Add the necessary routes:&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 config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:chats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:create&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:show&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="ss"&gt;:create&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;Considering AIs can take a bit of time to "think", we're making the call in a background 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;GenerateAiResponseJob&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;chat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Chat&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;chat_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;thinking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

    &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_message&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;chunk&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thinking&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
        &lt;span class="n"&gt;thinking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
        &lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_append_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;"chat_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;chat&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="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"conversation-log"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"messages/message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_append_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"chat_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;chat&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="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"message_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&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;_content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&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;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ask&lt;/code&gt; method from RubyLLM will add 2 new messages to the chat. The first one is the message from the user and the second is for the AI's response. The response from the LLM comes back from the provider in chunks and each chunk is passed to the block provided. We wait for the first non-empty chunk before appending the chat's last message (the one created for the AI) to the conversation log. After that we can stream the content of subsequent chunks and append them to the message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; You can customize the AI's behavior by adding system prompts to the chat instance, see the &lt;a href="https://rubyllm.com/guides/chat#system-prompts" rel="noopener noreferrer"&gt;RubyLLM docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we create the views:&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="c"&gt;&amp;lt;%# app/views/chats/index.html.erb %&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;h1&amp;gt;&lt;/span&gt;Chats&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@chats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;No chats found. Create a new chat.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&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="vi"&gt;@chats&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;chat&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="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;ID: &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'View'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/span&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;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;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;link_to&lt;/span&gt; &lt;span class="s1"&gt;'New Chat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chats_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;turbo_method: :post&lt;/span&gt; &lt;span class="p"&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="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;


&lt;span class="c"&gt;&amp;lt;%# app/views/chats/show.html.erb %&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"chat"&lt;/span&gt; &lt;span class="na"&gt;data-chat-id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="s2"&gt;"chat_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@chat&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="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;h1&amp;gt;&lt;/span&gt;Chat #&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Created: &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="p"&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="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"conversation-log"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&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;render&lt;/span&gt; &lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&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="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;No messages yet.&lt;span class="nt"&gt;&amp;lt;/p&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;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;render&lt;/span&gt; &lt;span class="s2"&gt;"messages/form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;chat: &lt;/span&gt;&lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&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;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Back to Chats'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chats_path&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="c"&gt;&amp;lt;%# app/views/messages/_message.html.erb %&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&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;span&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;role&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;:&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%H:%M:%S'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&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&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;_content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&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="c"&gt;&amp;lt;%# app/views/messages/_form.html.erb %&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&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;chat_messages_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"new_message"&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;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;text_area&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Enter message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;rows: &lt;/span&gt;&lt;span class="mi"&gt;2&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;submit&lt;/span&gt; &lt;span class="s2"&gt;"Send"&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="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;p&gt;And that's it!&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Now you should have a working AI chat that allows users to have persistent conversations with AI models. In terms of usefulness to your app, this is only the beginning. The real power comes when we let the AI interact with our application's data and functionality through Tools.&lt;br&gt;
If you were to set this up in an e-commerce app, you could use tools to allow an AI to check inventory, calculate shipping costs or search for a specific order. We'll dive into this and explain tools in the next post.&lt;/p&gt;

&lt;p&gt;For now, try adding this to your own Rails app and don't forget to add some proper error handling and security measures before deploying to production.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Re-Revisiting Performance in Ruby 3.4.1</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Wed, 26 Mar 2025 02:38:14 +0000</pubDate>
      <link>https://dev.to/reinteractive/re-revisiting-performance-in-ruby-341-2g9m</link>
      <guid>https://dev.to/reinteractive/re-revisiting-performance-in-ruby-341-2g9m</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/ruby-performance-structs-classes" rel="noopener noreferrer"&gt;Miko Dagatan&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Updated: 27 Mar 2025&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this article, I’ll be benchmarking Ruby 3.4.2, I’ve had my previous article, &lt;a href="https://reinteractive.com/articles/tutorial-series-for-experienced-rails-developers/ruby-3-4-1-struct-class-performance" rel="noopener noreferrer"&gt;Revisiting Performance in Ruby 3.4.1&lt;/a&gt;, published and have received various reactions regarding it through this &lt;a href="https://www.reddit.com/r/ruby/comments/1j6yj2l/revisiting_performance_in_ruby_341/" rel="noopener noreferrer"&gt;reddit page&lt;/a&gt;. I would like to say I'm very thankful for those who have provided their feedback so that I could improve on benchmarking code and presenting my observations.&lt;/p&gt;

&lt;p&gt;There are 3 points that have come to importance from all the feedback:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The articles (&lt;a href="https://alchemists.io/articles/ruby_structs" rel="noopener noreferrer"&gt;the first, which is an Alchemist article&lt;/a&gt; and &lt;a href="https://thiagolimadeveloper.medium.com/structs-in-ruby-b22c99e07208" rel="noopener noreferrer"&gt;second, which is a medium article&lt;/a&gt;) I provided do not support my past observation that &lt;code&gt;"Structs are powerful and could be used to define some of the code in place of the Class"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;benchmark-ips&lt;/code&gt; to better benchmark the code I'm benchmarking.&lt;/li&gt;
&lt;li&gt;My new conclusion that &lt;code&gt;Classes are now faster than Structs&lt;/code&gt; holds false when I use &lt;code&gt;benchmark-ips&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I understand these points challenge my observations and I would like to further dive deeper to support my initial findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Past observation: Structs are powerful and could be used to define some of the code in place of the Class
&lt;/h2&gt;

&lt;p&gt;I've been reading articles and comments that claim Structs could be used instead of other code. Some said in place of Hashes, some said in place of Classes. Structs provide structure, organisation, and readability to your data so it's better to use instead of Hashes in that regard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://stackoverflow.com/a/27419250" rel="noopener noreferrer"&gt;This Stack Overflow comment&lt;/a&gt; said that structs are better than hashes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stackoverflow.com/a/65550999" rel="noopener noreferrer"&gt;This Stack Overflow comment&lt;/a&gt; said that in ruby 3.0.0p0, structs are faster than hashes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stackoverflow.com/a/31314908" rel="noopener noreferrer"&gt;This Stack Overflow comment points out the real advantage of Structs in 2015&lt;/a&gt; and that is it's faster.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gist.github.com/tristang/a961fdc327e7d79439729a6c381227dc" rel="noopener noreferrer"&gt;This GitHub gist that compares hashes, ostructs, and structs&lt;/a&gt; shows the initialisation (write-only) of data. Hashes are faster in here, but there's still a clear advantage of using Structs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forum.crystal-lang.org/t/performance-struct-vs-class/1715" rel="noopener noreferrer"&gt;This Crystal forum page shows that in 2020, structs are faster than classes&lt;/a&gt;. Things like these, practically performance comparisons with classes, are what appealed to me.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.honeybadger.io/blog/how-openstruct-and-hashes-can-kill-performance/" rel="noopener noreferrer"&gt;This Honeybadger article's point is to not use Open Structs.&lt;/a&gt; However, it also shows in the benchmarks that Structs were faster compared to the classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, there you go. I've added more links to help give a general understanding of what I understood the majority claims in previous years, that Structs are faster than Classes, and it's great to make use of it as much as possible when your coding situation permits it. The &lt;a href="https://alchemists.io/articles/ruby_structs" rel="noopener noreferrer"&gt;Alchemist article&lt;/a&gt; provides a great explanation on when to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should have checked three times!
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://reinteractive.com/articles/tutorial-series-for-experienced-rails-developers/ruby-3-4-1-struct-class-performance" rel="noopener noreferrer"&gt;previous article&lt;/a&gt; I've claimed that throughout the years, Ruby may have improved Classes to the point that in certain cases they are faster than Structs. When I initially tested it, I was shocked to find it out, and was very excited to share it to the world. I made adjustments to the benchmarks to ensure that I'm definitely seeing this correctly. Then I've put the article for the world to see.&lt;/p&gt;

&lt;p&gt;One of the first comments in the &lt;a href="https://www.reddit.com/r/ruby/comments/1j6yj2l/revisiting_performance_in_ruby_341/" rel="noopener noreferrer"&gt;Reddit thread&lt;/a&gt; was a suggestion to use &lt;code&gt;benchmark-ips&lt;/code&gt;, and that my code should separate the reads and the writes. I followed his advice on the &lt;code&gt;benchmark-ips&lt;/code&gt; but while trying to &lt;code&gt;retain my code (to explain later)&lt;/code&gt;, and what do you know? Turns out that Struct is still faster than Classes. I've been wrong about it! I guessed that I should have probably checked three times before!&lt;/p&gt;

&lt;p&gt;Here's the result when using the &lt;code&gt;benchamrk-ips&lt;/code&gt; to my benchmarking code. &lt;code&gt;attr_reader&lt;/code&gt; is the Class object.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]

Calculating -------------------------------------
               array     27.014M (± 1.6%) i/s   (37.02 ns/i) -    136.720M in   5.062568s
            sym_hash     21.751M (± 2.4%) i/s   (45.98 ns/i) -    110.675M in   5.091684s
            str_hash     20.719M (± 4.6%) i/s   (48.27 ns/i) -    105.263M in   5.094066s
         attr_reader      7.954M (± 1.0%) i/s  (125.72 ns/i) -     40.392M in   5.078593s
              struct     10.973M (± 1.7%) i/s   (91.13 ns/i) -     54.974M in   5.011294s
                data      6.813M (± 1.3%) i/s  (146.77 ns/i) -     34.326M in   5.038833s

Comparison:
               array: 27013631.8 i/s
            sym_hash: 21750676.4 i/s - 1.24x  slower
            str_hash: 20718679.0 i/s - 1.30x  slower
              struct: 10973472.4 i/s - 2.46x  slower
         attr_reader:  7954235.5 i/s - 3.40x  slower
                data:  6813492.5 i/s - 3.96x  slower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  An unbelievable twist!
&lt;/h2&gt;

&lt;p&gt;There was a comment that came about in the &lt;a href="https://www.reddit.com/r/ruby/comments/1j6yj2l/revisiting_performance_in_ruby_341/" rel="noopener noreferrer"&gt;Reddit thread&lt;/a&gt;. I've already spent days trying to grind at my job. So I forgot to check on it. The commenter said &lt;code&gt;"Am I reading the same articles? The first(Alchemist) articles mentions that OpenStruct is terrible for performance (among other reasons), and it states "Performance has waned recently where structs used to be more performant than classes"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It was odd for me because I definitely understood that the articles I referenced are promoting the use of Structs and support my understanding that the general opinion is to &lt;code&gt;make use of them when you can over classes and hashes&lt;/code&gt;. So, I re-read both articles, &lt;a href="https://thiagolimadeveloper.medium.com/structs-in-ruby-b22c99e07208" rel="noopener noreferrer"&gt;Medium article&lt;/a&gt;, which was a faster read, then the &lt;a href="https://alchemists.io/articles/ruby_structs" rel="noopener noreferrer"&gt;Alchemist article&lt;/a&gt;. This took a long time, but I enjoyed re-reading it. I noticed that the writer of the article wrote &lt;code&gt;"Performance has waned recently where structs used to be more performant than classes"&lt;/code&gt; in the article, and I was sure that I never read that before. I took a look at when it's last updated. Turns out it got updated after I wrote the article, and the Alchemist article got updated the same day as my previous article. &lt;code&gt;February 4, 2025&lt;/code&gt; That makes sense, now I understand why some readers looked confused in their comments about it.&lt;/p&gt;

&lt;p&gt;What strikes me is that the Alchemist article changed its stance to support the claim I made in my previous article! Yes, indeed, my article became thoroughly confusing because of that. However, it's more interesting that the Alchemist article supports my initial claim!&lt;/p&gt;

&lt;p&gt;The article's benchmark was great because it has 5 attributes instantiated into the objects. It's closer to real-life use, as we're silly to simply use these different data structures, yet provide only one attribute.&lt;/p&gt;

&lt;p&gt;I'll copy the code it provided, but I'll try to add more code into it to provide more scenarios. Let's see how these things fare in 2025.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Benchmark both Read &amp;amp; Write?
&lt;/h2&gt;

&lt;p&gt;When benchmarking these objects A reddit user mentioned that it's best to test the &lt;code&gt;read&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt; of the objects in isolation. However, I cannot agree with that as I see in the multitude of codebases I've touched, there's always a &lt;code&gt;write&lt;/code&gt; and there can be more than one &lt;code&gt;read&lt;/code&gt; when using these objects. I prefer to be close to the real life scenarios.&lt;/p&gt;

&lt;p&gt;In my previous article's benchmarks, I've only simulated 1:1 read-write benchmarking. But today, I'll double down on this perspective and benchmark 1:1, 2:1, 3:1, 5:1, and 10:1 read-write situations. This will give us a better understanding of the real-life scenarios for these objects.&lt;/p&gt;
&lt;h2&gt;
  
  
  Benchmarking
&lt;/h2&gt;

&lt;p&gt;We're using the benchmarking code from the Alchemist's article, and we're adding a few more things there. Here's the new code for benchmarking. I've also added a "Hash string" test so that we can also determine the difference between symbolized hashes and stringified hashes (with frozen string literal comment). I didn't use YJIT for this case because there's already a lot of code and benchmarking results. Try the benchmarking code on your end for YJIT:&lt;/p&gt;
&lt;h2&gt;
  
  
  Benchmarking Code
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `benchmark`, then `chmod 755 benchmark`, and run as `./benchmark`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"
  gem "benchmark-ips"
  gem "debug"
  gem "ostruct"
end

Warning[:performance] = false

require "ostruct"

DataDemo = Data.define :a, :b, :c, :d, :e
StructDemo = Struct.new :a, :b, :c, :d, :e

ClassDemo = Class.new do
  attr_reader :a, :b, :c, :d, :e

  def initialize a:, b:, c:, d:, e:
    u/a = a
    u/b = b
    u/c = c
    u/d = d
    u/e = e
  end
end

DataDemoTen = Data.define(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j)
StructDemoTen = Struct.new(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j)

ClassDemoTen = Class.new do
  attr_reader :a, :b, :c, :d, :e, :f, :g, :h, :i, :j

  def initialize a:, b:, c:, d:, e:, f:, g:, h:, i:, j:
    u/a = a
    u/b = b
    @c = c
    @d = d
    @e = e
    @f = f
    @g = g
    @h = h
    @i = i
    @j = j
  end
end



puts "--- 1 Read to 1 Write ---"
Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0] }
  benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a] }
  benchmark.report("Hash String") { hash = {'a' =&amp;gt; 1, 'b' =&amp;gt; 2, 'c' =&amp;gt; 3, 'd' =&amp;gt; 4, 'e' =&amp;gt; 5}; hash['a'] }
  benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a }
  benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a }
  benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a }
  benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a }

  benchmark.compare!
end

puts "--- 2 Reads to 1 Write ---"
Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0]; arr[1] }
  benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a]; hash[:b] }
  benchmark.report("Hash String") { hash = {'a' =&amp;gt; 1, 'b' =&amp;gt; 2, 'c' =&amp;gt; 3, 'd' =&amp;gt; 4, 'e' =&amp;gt; 5}; hash['a']; hash['b'] }
  benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a; data.b }
  benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a; struct.b }
  benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a; ostruct.b }
  benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a; klass.b }

  benchmark.compare!
end

puts "--- 3 Reads to 1 Write ---"
Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0]; arr[1]; arr[2] }
  benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a]; hash[:b]; hash[:c] }
  benchmark.report("Hash String") { hash = {'a' =&amp;gt; 1, 'b' =&amp;gt; 2, 'c' =&amp;gt; 3, 'd' =&amp;gt; 4, 'e' =&amp;gt; 5}; hash['a']; hash['b']; hash['c'] }
  benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a; data.b; data.c }
  benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a; struct.b; struct.c }
  benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a; ostruct.b; ostruct.c }
  benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a; klass.b; klass.c }

  benchmark.compare!
end

puts "--- 5 Reads to 1 Write ---"
Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0]; arr[1]; arr[2]; arr[3]; arr[4] }
  benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e] }
  benchmark.report("Hash String") { hash = {'a' =&amp;gt; 1, 'b' =&amp;gt; 2, 'c' =&amp;gt; 3, 'd' =&amp;gt; 4, 'e' =&amp;gt; 5}; hash['a']; hash['b']; hash['c']; hash['d']; hash['e'] }
  benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a; data.b; data.c; data.d; data.e }
  benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a; struct.b; struct.c; struct.d; struct.e }
  benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a; ostruct.b; ostruct.c; ostruct.d; ostruct.e }
  benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a; klass.b; klass.c; klass.d; klass.e }

  benchmark.compare!
end

puts "--- 10 Reads to 1 Write (5 attributes) ---"
Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Array") { arr = [1, 2, 3, 4, 5]; arr[0]; arr[1]; arr[2]; arr[3]; arr[4]; arr[0]; arr[1]; arr[2]; arr[3]; arr[4] }
  benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5}; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e]; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e] }
  benchmark.report("Hash String") { hash = {'a' =&amp;gt; 1, 'b' =&amp;gt; 2, 'c' =&amp;gt; 3, 'd' =&amp;gt; 4, 'e' =&amp;gt; 5}; hash['a']; hash['b']; hash['c']; hash['d']; hash['e']; hash['a']; hash['b']; hash['c']; hash['d']; hash['e'] }
  benchmark.report("Data") { data = DataDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; data.a; data.b; data.c; data.d; data.e; data.a; data.b; data.c; data.d; data.e }
  benchmark.report("Struct") { struct = StructDemo[a: 1, b: 2, c: 3, d: 4, e: 5]; struct.a; struct.b; struct.c; struct.d; struct.e; struct.a; struct.b; struct.c; struct.d; struct.e }
  benchmark.report("OpenStruct") { ostruct = OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5; ostruct.a; ostruct.b; ostruct.c; ostruct.d; ostruct.e; ostruct.a; ostruct.b; ostruct.c; ostruct.d; ostruct.e }
  benchmark.report("Class") { klass = ClassDemo.new a: 1, b: 2, c: 3, d: 4, e: 5; klass.a; klass.b; klass.c; klass.d; klass.e; klass.a; klass.b; klass.c; klass.d; klass.e }

  benchmark.compare!
end

puts "--- 10 Reads to 1 Write (10 attributes) ---"
Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Array") { arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; arr[0]; arr[1]; arr[2]; arr[3]; arr[4]; arr[5]; arr[6]; arr[7]; arr[8]; arr[9] }
  benchmark.report("Hash") { hash = {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10}; hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e]; hash[:f]; hash[:g]; hash[:h]; hash[:i]; hash[:j] }
  benchmark.report("Hash String") { hash = {'a' =&amp;gt; 1, 'b' =&amp;gt; 2, 'c' =&amp;gt; 3, 'd' =&amp;gt; 4, 'e' =&amp;gt; 5, 'f' =&amp;gt; 6, 'g' =&amp;gt; 7, 'h' =&amp;gt; 8, 'i' =&amp;gt; 9, 'j' =&amp;gt; 10}; hash['a']; hash['b']; hash['c']; hash['d']; hash['e']; hash['f']; hash['g']; hash['h']; hash['i']; hash['j'] }
  benchmark.report("Data") { data = DataDemoTen.new(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10); data.a; data.b; data.c; data.d; data.e; data.f; data.g; data.h; data.i; data.j }
  benchmark.report("Struct") { struct = StructDemoTen.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); struct.a; struct.b; struct.c; struct.d; struct.e; struct.f; struct.g; struct.h; struct.i; struct.j }
  benchmark.report("OpenStruct") { ostruct = OpenStruct.new(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10); ostruct.a; ostruct.b; ostruct.c; ostruct.d; ostruct.e; ostruct.f; ostruct.g; ostruct.h; ostruct.i; ostruct.j }
  benchmark.report("Class") { klass = ClassDemoTen.new(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10); klass.a; klass.b; klass.c; klass.d; klass.e; klass.f; klass.g; klass.h; klass.i; klass.j }

  benchmark.compare!
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1 read to 1 write
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculating -------------------------------------&lt;br&gt;
               Array     25.764M (± 0.9%) i/s   (38.81 ns/i) -    128.815M in   5.000339s&lt;br&gt;
                Hash     21.860M (± 0.4%) i/s   (45.75 ns/i) -    111.235M in   5.088522s&lt;br&gt;
         Hash String     20.215M (± 0.4%) i/s   (49.47 ns/i) -    102.154M in   5.053419s&lt;br&gt;
                Data      4.158M (± 2.3%) i/s  (240.52 ns/i) -     21.125M in   5.083854s&lt;br&gt;
              Struct      4.101M (± 1.9%) i/s  (243.83 ns/i) -     20.603M in   5.025646s&lt;br&gt;
          OpenStruct    122.586k (± 0.7%) i/s    (8.16 μs/i) -    616.400k in   5.028558s&lt;br&gt;
               Class      4.540M (± 1.7%) i/s  (220.25 ns/i) -     22.995M in   5.066432s

&lt;p&gt;Comparison:&lt;br&gt;
               Array: 25763513.4 i/s&lt;br&gt;
                Hash: 21860209.5 i/s - 1.18x  slower&lt;br&gt;
         Hash String: 20215108.6 i/s - 1.27x  slower&lt;br&gt;
               Class:  4540193.3 i/s - 5.67x  slower&lt;br&gt;
                Data:  4157661.5 i/s - 6.20x  slower&lt;br&gt;
              Struct:  4101170.2 i/s - 6.28x  slower&lt;br&gt;
          OpenStruct:   122586.4 i/s - 210.17x  slower&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  2 reads to 1 write&lt;br&gt;
&lt;/h2&gt;
&lt;br&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculating -------------------------------------&lt;br&gt;
               Array     20.633M (± 0.7%) i/s   (48.47 ns/i) -    103.215M in   5.002565s&lt;br&gt;
                Hash     18.106M (± 1.2%) i/s   (55.23 ns/i) -     92.276M in   5.097036s&lt;br&gt;
         Hash String     16.850M (± 0.4%) i/s   (59.35 ns/i) -     84.474M in   5.013416s&lt;br&gt;
                Data      4.088M (± 2.0%) i/s  (244.64 ns/i) -     20.519M in   5.021858s&lt;br&gt;
              Struct      4.034M (± 1.6%) i/s  (247.90 ns/i) -     20.316M in   5.037631s&lt;br&gt;
          OpenStruct    120.040k (± 1.0%) i/s    (8.33 μs/i) -    605.064k in   5.041019s&lt;br&gt;
               Class      4.440M (± 1.5%) i/s  (225.21 ns/i) -     22.449M in   5.056871s

&lt;p&gt;Comparison:&lt;br&gt;
               Array: 20633383.1 i/s&lt;br&gt;
                Hash: 18106481.9 i/s - 1.14x  slower&lt;br&gt;
         Hash String: 16849875.2 i/s - 1.22x  slower&lt;br&gt;
               Class:  4440226.6 i/s - 4.65x  slower&lt;br&gt;
                Data:  4087571.4 i/s - 5.05x  slower&lt;br&gt;
              Struct:  4033868.2 i/s - 5.12x  slower&lt;br&gt;
          OpenStruct:   120039.9 i/s - 171.89x  slower&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  3 reads to 1 write&lt;br&gt;
&lt;/h2&gt;
&lt;br&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculating -------------------------------------&lt;br&gt;
               Array     18.320M (± 0.9%) i/s   (54.58 ns/i) -     92.829M in   5.067386s&lt;br&gt;
                Hash     16.198M (± 0.4%) i/s   (61.74 ns/i) -     82.530M in   5.095210s&lt;br&gt;
         Hash String     14.845M (± 0.8%) i/s   (67.36 ns/i) -     74.947M in   5.048993s&lt;br&gt;
                Data      3.993M (± 2.6%) i/s  (250.45 ns/i) -     20.235M in   5.071372s&lt;br&gt;
              Struct      3.721M (± 8.3%) i/s  (268.72 ns/i) -     18.474M in   5.030555s&lt;br&gt;
          OpenStruct    109.286k (±16.7%) i/s    (9.15 μs/i) -    504.820k in   5.042702s&lt;br&gt;
               Class      4.311M (± 1.8%) i/s  (231.98 ns/i) -     21.626M in   5.018517s

&lt;p&gt;Comparison:&lt;br&gt;
               Array: 18320261.7 i/s&lt;br&gt;
                Hash: 16197886.7 i/s - 1.13x  slower&lt;br&gt;
         Hash String: 14844935.0 i/s - 1.23x  slower&lt;br&gt;
               Class:  4310699.6 i/s - 4.25x  slower&lt;br&gt;
                Data:  3992742.9 i/s - 4.59x  slower&lt;br&gt;
              Struct:  3721375.0 i/s - 4.92x  slower&lt;br&gt;
          OpenStruct:   109285.6 i/s - 167.64x  slower&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  5 reads to 1 write&lt;br&gt;
&lt;/h2&gt;
&lt;br&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculating -------------------------------------&lt;br&gt;
               Array     15.308M (± 2.2%) i/s   (65.32 ns/i) -     77.630M in   5.073563s&lt;br&gt;
                Hash     14.129M (±21.2%) i/s   (70.78 ns/i) -     64.798M in   4.984178s&lt;br&gt;
         Hash String     12.384M (± 1.6%) i/s   (80.75 ns/i) -     62.810M in   5.073061s&lt;br&gt;
                Data      3.740M (± 2.1%) i/s  (267.40 ns/i) -     18.929M in   5.063717s&lt;br&gt;
              Struct      3.731M (± 1.6%) i/s  (267.99 ns/i) -     18.722M in   5.018610s&lt;br&gt;
          OpenStruct    114.473k (± 1.0%) i/s    (8.74 μs/i) -    578.442k in   5.053565s&lt;br&gt;
               Class      4.142M (± 1.2%) i/s  (241.42 ns/i) -     20.902M in   5.046783s

&lt;p&gt;Comparison:&lt;br&gt;
               Array: 15308341.0 i/s&lt;br&gt;
                Hash: 14129046.7 i/s - same-ish: difference falls within error&lt;br&gt;
         Hash String: 12384465.8 i/s - 1.24x  slower&lt;br&gt;
               Class:  4142199.3 i/s - 3.70x  slower&lt;br&gt;
                Data:  3739735.6 i/s - 4.09x  slower&lt;br&gt;
              Struct:  3731458.8 i/s - 4.10x  slower&lt;br&gt;
          OpenStruct:   114472.7 i/s - 133.73x  slower&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  10 reads to 1 write -- 5 attributes&lt;br&gt;
&lt;/h2&gt;
&lt;br&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculating -------------------------------------&lt;br&gt;
               Array     10.777M (± 0.7%) i/s   (92.79 ns/i) -     54.867M in   5.091625s&lt;br&gt;
                Hash      8.618M (± 1.2%) i/s  (116.04 ns/i) -     43.838M in   5.087596s&lt;br&gt;
         Hash String      7.962M (± 1.0%) i/s  (125.60 ns/i) -     40.308M in   5.062987s&lt;br&gt;
                Data      3.440M (± 1.8%) i/s  (290.74 ns/i) -     17.278M in   5.025126s&lt;br&gt;
              Struct      3.416M (± 1.2%) i/s  (292.75 ns/i) -     17.342M in   5.077662s&lt;br&gt;
          OpenStruct    113.327k (± 0.8%) i/s    (8.82 μs/i) -    568.500k in   5.016805s&lt;br&gt;
               Class      3.576M (± 1.4%) i/s  (279.64 ns/i) -     18.189M in   5.087567s

&lt;p&gt;Comparison:&lt;br&gt;
               Array: 10776504.7 i/s&lt;br&gt;
                Hash:  8617805.7 i/s - 1.25x  slower&lt;br&gt;
         Hash String:  7962076.7 i/s - 1.35x  slower&lt;br&gt;
               Class:  3575982.6 i/s - 3.01x  slower&lt;br&gt;
                Data:  3439532.9 i/s - 3.13x  slower&lt;br&gt;
              Struct:  3415890.4 i/s - 3.15x  slower&lt;br&gt;
          OpenStruct:   113326.8 i/s - 95.09x  slower&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  10 reads to 1 write -- 10 attributes&lt;br&gt;
&lt;/h2&gt;
&lt;br&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculating -------------------------------------&lt;br&gt;
               Array     10.711M (± 0.6%) i/s   (93.36 ns/i) -     53.734M in   5.016788s&lt;br&gt;
                Hash      4.271M (± 1.7%) i/s  (234.13 ns/i) -     21.775M in   5.099576s&lt;br&gt;
         Hash String      3.852M (± 2.0%) i/s  (259.59 ns/i) -     19.270M in   5.004605s&lt;br&gt;
                Data      1.923M (± 2.3%) i/s  (520.06 ns/i) -      9.783M in   5.090560s&lt;br&gt;
              Struct      6.601M (± 1.5%) i/s  (151.49 ns/i) -     33.252M in   5.038446s&lt;br&gt;
          OpenStruct     59.513k (± 1.7%) i/s   (16.80 μs/i) -    297.550k in   5.001175s&lt;br&gt;
               Class      1.885M (± 1.4%) i/s  (530.46 ns/i) -      9.427M in   5.001555s

&lt;p&gt;Comparison:&lt;br&gt;
               Array: 10711256.7 i/s&lt;br&gt;
              Struct:  6601054.0 i/s - 1.62x  slower&lt;br&gt;
                Hash:  4271204.4 i/s - 2.51x  slower&lt;br&gt;
         Hash String:  3852188.8 i/s - 2.78x  slower&lt;br&gt;
                Data:  1922861.9 i/s - 5.57x  slower&lt;br&gt;
               Class:  1885149.8 i/s - 5.68x  slower&lt;br&gt;
          OpenStruct:    59513.5 i/s - 179.98x  slower&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Current Observations - What a rollercoaster!&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;That's a lot of benchmarking! I was really hoping that with many reads, that Structs comes out more performant and it did! So, I'm happy with the results. What we can see is that Structs have performed very well even compared to Hashes when there are many attributes, in this case, in the &lt;code&gt;10 reads to 1 write -- 10 attributes&lt;/code&gt;. So, while we are grateful that Classes has gotten more performant than Struct in the 5 attribute case, but Struct still is a great choice as a standard when passing around data, due to its good scalability.&lt;/p&gt;

&lt;p&gt;Stringified Hashes are also performant under the frozen string literal comment, so there's not much impact on using between symbolized and stringified Hashes.&lt;/p&gt;

&lt;p&gt;&lt;del&gt;## Surprising Observation&lt;/del&gt;&lt;/p&gt;

&lt;p&gt;&lt;del&gt;What surprises me is how exponentially slow the Data, Classes, and Structs are when dealing with 10 attributes. Having 50-60 times slower performance than Arrays has got to be excruciatingly painful on dealing with.&lt;/del&gt; &lt;code&gt;(Hash to - Class: 21.68x, Hash to - Data: 19.77, Hash to Struct - 24.26x)&lt;/code&gt; &lt;del&gt;So, if you're dealing with large data (well, 10 seems large enough considering the impact), it would be best to use more primitive data objects, like Arrays and Hashes, especially Hashes since it has at least some structure on to it.&lt;/del&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5th Time
&lt;/h2&gt;

&lt;p&gt;Someone in this new reddit thread has pointed out to me that my &lt;code&gt;10 reads to 1 write -- 10 attributes&lt;/code&gt; case was written in such a way that we defined them inside the benchmark. I'm correcting the code, I have re-evaluated my observations once again. The mistake is what got me writing the Surprising Observation, wherein I thought that having more attributes greatly affects Classes, Structs, and Data compared to Hashes, but I was wrong. So, I'm very grateful for that as the correction has changed the narrative to recommend the usage of Structs vs Classes (and Hashes) if you're solely looking for performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Struct as a Value Object
&lt;/h2&gt;

&lt;p&gt;I think one of the most important thing with Structs (and Data) is that they're value objects. In my own words, it means that you can compare them by themselves. Class instances cannot be compared by themselves, and that's the only disadvantage I could see with classes, considering they're more performant in most cases now.&lt;/p&gt;

&lt;p&gt;Take a look at the Class code to show this behavior:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;irb(main):001* class A&lt;br&gt;
irb(main):002*   attr_reader :a&lt;br&gt;
irb(main):003*   def initialize(a)&lt;br&gt;
irb(main):004*     @a = a&lt;br&gt;
irb(main):005*   end&lt;br&gt;
irb(main):006&amp;gt; end&lt;br&gt;
=&amp;gt; :initialize&lt;br&gt;
irb(main):007&amp;gt; a = A.new(1)&lt;br&gt;
=&amp;gt; #&amp;lt;A:0x000000012529f560 @a=1&amp;gt;&lt;br&gt;
irb(main):008&amp;gt; b = A.new(1)&lt;br&gt;
=&amp;gt; #&amp;lt;A:0x000000011fb11488 @a=1&amp;gt;&lt;br&gt;
irb(main):009&amp;gt; a == b&lt;br&gt;
=&amp;gt; false&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Conclusion&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;I think it was a great decision to write this second article, because I've learned more things with the wonderful Ruby language. I hope you've enjoyed reading as I've enjoyed writing this.&lt;/p&gt;

&lt;p&gt;Here are my takeaways on this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Ruby 3.4.2, Classes are slightly more performant than Structs when we use 5 attributes, but with 10 attributes, Structs come out on top even compared to Hashes.&lt;/li&gt;
&lt;li&gt;The order of priority (in terms of scalable performance) when using data structures are Arrays, Hashes, Structs, Classes, Data. But of course, these get used differently. When you want more structure, Structs are definitely on top of the list.&lt;/li&gt;
&lt;li&gt;Symbolised Hashes are better than Stringified Hashes even with the frozen string literal comment, but not very far off.&lt;/li&gt;
&lt;li&gt;Always use the frozen string literal comment.&lt;/li&gt;
&lt;li&gt;Don't check twice, check 3, 4, 5 times!&lt;/li&gt;
&lt;li&gt;Articles you reference update themselves and make your referring article confusing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Real Time Page Updates with Rails and Hotwire - Turbo Broadcasts</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Mon, 24 Mar 2025 02:26:35 +0000</pubDate>
      <link>https://dev.to/reinteractive/real-time-page-updates-with-rails-and-hotwire-turbo-broadcasts-3fnn</link>
      <guid>https://dev.to/reinteractive/real-time-page-updates-with-rails-and-hotwire-turbo-broadcasts-3fnn</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/rails-turbo-realtime-updates" rel="noopener noreferrer"&gt;Charles Martinez&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hotwire has been the default frontend framework for Rails Application since Rails 7. And one of the most important framework within Hotwire is Turbo which uses multiple techniques to provide a SPA experience within our application.&lt;/p&gt;

&lt;p&gt;And one of the things I really like about Turbo is the ability to provide real time page updates quickly and easily and without having to write any javascript code with it.&lt;/p&gt;

&lt;p&gt;In this example, Let's say we have an Event app where you can register to, And we will apply real time page updates on any modifications to the Events table or whenever someones registers for an event. Below with be the end result we would want to achieve&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.amazonaws.com%2Fuploads%2Farticles%2Fl7h3rvxdle68xfwuv3ej.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fl7h3rvxdle68xfwuv3ej.gif" alt="Modal GIF" width="480" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Turbo Broadcast
&lt;/h3&gt;

&lt;p&gt;Turbo Broadcast allows us to broadcast messages via Websockets to multiple clients in real-time and which is what we will be using in this example. This is the source code for Turbo Broadcast and its worth taking a look at because it provides some example usages in the inline comments &lt;a href="https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb" rel="noopener noreferrer"&gt;https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So lets say we have an existing table of upcoming events, The first thing we need to do is inject this line &lt;code&gt;turbo_stream_from "events"&lt;/code&gt; within the html file that renders this table, and add an ID to the HTML element that contains the data and HTML elements we would want to update in real-time&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;main&amp;gt;
  &amp;lt;%= turbo_stream_from "events" %&amp;gt;  &amp;lt;!-- Add this line !--&amp;gt;

  &amp;lt;h3 class="header mb-4"&amp;gt; Upcoming Events &amp;lt;/h3&amp;gt;

  &amp;lt;table class="table"&amp;gt;
    &amp;lt;thead&amp;gt;
      &amp;lt;th&amp;gt; Event Name &amp;lt;/th&amp;gt;
      ...
    &amp;lt;/thead&amp;gt;

    &amp;lt;tbody id='eventsTable'&amp;gt; &amp;lt;!-- Assign an ID !--&amp;gt;
      &amp;lt;% @events.each do |event| %&amp;gt;
        &amp;lt;%= render partial: "event", locals: { event: event } %&amp;gt;
      &amp;lt;% end %&amp;gt;
    &amp;lt;/tbody&amp;gt;
  &amp;lt;/table&amp;gt;
&amp;lt;/main&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this do is establishes a websocket connection on that page to subscribes users to that channel. That helper method would produce something like this, where &lt;code&gt;signed-stream-name&lt;/code&gt; is the signed version of the passed string "events"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;turbo-cable-stream-source
  channel="Turbo::StreamsChannel"
  signed-stream-name="signed-version-of-passed-string"
&amp;gt;
&amp;lt;/turbo-cable-stream-source&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in this context, All of the users currently in that events page are subscribed to the &lt;code&gt;Turbo::StreamsChannel&lt;/code&gt; and waiting for broadcasts that will be made on the &lt;code&gt;events&lt;/code&gt; stream&lt;/p&gt;

&lt;p&gt;Also, on the &lt;code&gt;_event.html.erb&lt;/code&gt; partial, we needd to add an ID per each event row&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/events/_event.html.erb

&amp;lt;tr id="&amp;lt;%= dom_id event %&amp;gt;"&amp;gt;
  &amp;lt;td&amp;gt; &amp;lt;%= event.name %&amp;gt; &amp;lt;/td&amp;gt;
  ...
&amp;lt;/tr&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dom_id&lt;/code&gt; is a Rails helper that will return a string of the model name and ID e.g &lt;code&gt;event_1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now that we have that turbo stream setup and added the IDs that we needded, we need to add 3 lines of active record callbacks to the &lt;code&gt;Event&lt;/code&gt; model&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Event &amp;lt; ApplicationRecord
  include ActionView::RecordIdentifier
  has_many :bookings

  after_create_commit { broadcast_prepend_to "events", target: "eventsTable" }
  after_update_commit { broadcast_replace_to 'events', target: dom_id(self) }
  after_destroy_commit { broadcast_remove_to 'events', target: dom_id(self) }
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To explain further on, The broadcast method's first argument is the &lt;code&gt;stream_name&lt;/code&gt; which is &lt;code&gt;events&lt;/code&gt; coming from the stream name we've passed in &lt;code&gt;&amp;lt;%= turbo_stream_from "events" %&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;target&lt;/code&gt; parameter is the ID of the HTML element we would want to be modified. So you can notice that on create, We would want to modify the Table Body which we defined the ID as &lt;code&gt;eventsTable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And of course on update and destroy, we will modify the actual table row that the event is rendered to.&lt;/p&gt;

&lt;p&gt;It also accepts a parameter called &lt;code&gt;partial&lt;/code&gt;, But we don't need to add it in here. The naming convention that Turbo Broadcasts maps to by default will be based on the Model name. So in our case the Event Model, Turbo will then try to find a partial &lt;code&gt;/events/_event&lt;/code&gt; if the &lt;code&gt;partial&lt;/code&gt; parameter has not beed provided.&lt;/p&gt;

&lt;p&gt;A thing to note, We need to include &lt;code&gt;ActionView::RecordIdentifier&lt;/code&gt; so that we could use the &lt;code&gt;dom_id&lt;/code&gt; helper inside the Model class. And thats it! With these few lines of code, The Events page will receive real time updates given any modification, addition or deletion in the Events table.&lt;/p&gt;

&lt;p&gt;But this only covers any changes on the Event table, We need to be able to update the events page whenever a booking is created.&lt;/p&gt;

&lt;p&gt;There are two options, First, we can add &lt;code&gt;touch: true&lt;/code&gt; on that Booking model&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Booking &amp;lt; ApplicationRecord
  belongs_to :event, touch: true
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will update the associated &lt;code&gt;event's updated_at timestamp&lt;/code&gt; whenever a booking is created. But often times that not, This is not the behavior we intend to, So we can just define an active record callback as well to this model&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Event &amp;lt; ApplicationRecordd
  ...
  after_update_commit { broadcast_updates! }

  def broadcast_updates!
    broadcast_prepend_to "events", target: "eventsTable"
  end
end

class Booking &amp;lt; ApplicationRecord
  belongs_to :event

  after_create_commit { event.broadcast_updates! }
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We define a reusable instance method for broadcasting update changes so that we can define it both on the Event and Booking Model. And now we have real time page updates whenever someone registers for an event&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.amazonaws.com%2Fuploads%2Farticles%2Fbas8lpfg1cmto4pumdaq.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fbas8lpfg1cmto4pumdaq.gif" alt="GIF 2" width="480" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Loading and Transitions on broadcasts
&lt;/h3&gt;

&lt;p&gt;We have setup real time page updates on the events page, But ideally we want to be able to improve the user experience by adding loading and transitions whenever something changes on the events page. We can do that by adding and updating a few lines of code.&lt;/p&gt;

&lt;p&gt;First thing, we need to add another event partial that will render a loading row, In this context, Im using Bootstrap spinner for simplicity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/event/_loading_event.html

&amp;lt;tr id="&amp;lt;%= dom_id event %&amp;gt;"&amp;gt;
  &amp;lt;td&amp;gt; &amp;lt;div class="spinner-border" role="status"&amp;gt;&amp;lt;/td&amp;gt;
  &amp;lt;% 4.times do %&amp;gt;
    &amp;lt;td&amp;gt; ... &amp;lt;/td&amp;gt;
  &amp;lt;% end %&amp;gt;
&amp;lt;/tr&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;Which would look like this&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.amazonaws.com%2Fuploads%2Farticles%2Fzrrkw7n8btwngt3q0wof.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.amazonaws.com%2Fuploads%2Farticles%2Fzrrkw7n8btwngt3q0wof.png" alt="IMAGE 1" width="640" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we would want to add a simple in out transition css class, and allow the event partial to receive an optional transition_class parameter&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CSS

.in-out {
  animation: fade-in 0.5s ease-out,
             slide-in 0.5s ease-out;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/events/_event.html.erb

&amp;lt;% transition_class ||= nil %&amp;gt;

&amp;lt;tr id="&amp;lt;%= dom_id event %&amp;gt;" class="&amp;lt;%= transition_class %&amp;gt;"&amp;gt;
  ...
&amp;lt;/tr&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the Events Model, We would want to change our callbacks&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Event &amp;lt; ApplicationRecord
  ...

  after_create_commit { broadcast_create! }
  after_update_commit { broadcast_updates! }
  after_destroy_commit { broadcast_remove_to 'events', target: dom_id(self) }

  def broadcast_create!
    broadcast_prepend_to "events", target: "eventsTable", partial: "/events/loading_event"
    sleep(0.5)
    broadcast_replace_to 'events', target: dom_id(self), locals: { event: self, transition_class: "in-out" }
  end

  def broadcast_updates!
    broadcast_replace_to 'events', target: dom_id(self) , partial: "/events/loading_event"
    sleep(0.5)
    broadcast_replace_to 'events', target: dom_id(self), locals: { event: self, transition_class: "in-out" }
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On event creations and updates, We will broadcast 2 messages,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;To Load the loading event partial

&lt;ul&gt;
&lt;li&gt;Notice here, That we explicitly passed the partial argument, By default, The Turbo Broadcast will find a partial based on the model name e.g if the model name is Event, it would look for &lt;code&gt;/app/views/events/_event.html&lt;/code&gt;. That is the reason we didn't need to pass the partial argument previously.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;And after some delay, replace the loading event partial with the actual event partial with updated data

&lt;ul&gt;
&lt;li&gt;And notice that we are passing the &lt;code&gt;in_out&lt;/code&gt; transition class to give an transition effect when the turbo stream renders the updated element&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the end result with would be something like this&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.amazonaws.com%2Fuploads%2Farticles%2F2urs2txql7s7n2magt6y.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F2urs2txql7s7n2magt6y.gif" alt="GIF 3" width="480" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's a wrap! Thanks to Turbo, With a few lines of code, We can implement real time page updates on our application with a few lines of code.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Ruby on Rails Chat Application with ActionCable and Heroku</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Thu, 20 Mar 2025 01:56:28 +0000</pubDate>
      <link>https://dev.to/reinteractive/building-a-ruby-on-rails-chat-application-with-actioncable-and-heroku-4mpa</link>
      <guid>https://dev.to/reinteractive/building-a-ruby-on-rails-chat-application-with-actioncable-and-heroku-4mpa</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/rails-realtime-chat-turbo-cable" rel="noopener noreferrer"&gt;Rodrigo Souza&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this guide we'll create a real-time chat application using Rails 8.0.1 and Ruby 3.4.2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"https://rubygems.org"&lt;/span&gt;

&lt;span class="n"&gt;ruby&lt;/span&gt; &lt;span class="s2"&gt;"3.4.2"&lt;/span&gt;

&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"rails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 8.0.1"&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"turbo-rails"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;To get started, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proper Action Cable configuration in cable.yml&lt;/li&gt;
&lt;li&gt;Working user authentication system&lt;/li&gt;
&lt;li&gt;Turbo Rails properly installed and configured&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This implementation provides a robust foundation for a real-time chat application, leveraging Rails 8's modern features for seamless real-time updates with minimal JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Technical Aspects
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Turbo Streams and Broadcasting
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Turbo Streams: Handles real-time updates through WebSocket connections&lt;/li&gt;
&lt;li&gt;Action Cable: Powers the WebSocket functionality (built into Rails)&lt;/li&gt;
&lt;li&gt;Scoped Broadcasting: Messages only broadcast to specific room subscribers&lt;/li&gt;
&lt;li&gt;Partial Rendering: Keeps code DRY and maintains consistent UI updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's break down the key broadcasting mechanisms:&lt;/p&gt;

&lt;h4&gt;
  
  
  Room Broadcasting:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;broadcasts_to&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;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This establishes the room as a broadcast target, allowing Turbo to track changes to the room itself.&lt;/p&gt;

&lt;h4&gt;
  
  
  Message Broadcasting:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;after_create_commit&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;broadcast_append_to&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures new messages are automatically broadcast to all room subscribers.&lt;/p&gt;

&lt;h4&gt;
  
  
  JavaScript Integration
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Stimulus: Manages form behavior and DOM interactions&lt;/li&gt;
&lt;li&gt;Minimal JavaScript: Most real-time functionality handled by Turbo&lt;/li&gt;
&lt;li&gt;Automatic DOM Updates: No manual DOM manipulation required&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Models
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Room Model
&lt;/h5&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;Room&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;:messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;dependent: :destroy&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="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;uniqueness: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;broadcasts_to&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;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;room&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;h5&gt;
  
  
  Message Model
&lt;/h5&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;Message&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;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:room&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:content&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;after_create_commit&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;broadcast_append_to&lt;/span&gt; &lt;span class="n"&gt;room&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;h4&gt;
  
  
  Controllers
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Rooms Controller
&lt;/h5&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;RoomsController&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;index&lt;/span&gt;
    &lt;span class="vi"&gt;@rooms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&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;show&lt;/span&gt;
    &lt;span class="vi"&gt;@room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Room&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Message&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;@room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Room&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;room_params&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;@room&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;room_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;:room&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="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;h5&gt;
  
  
  Messages Controller
&lt;/h5&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;MessagesController&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;@message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Message&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;message_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;create_anonymous_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
    &lt;span class="vi"&gt;@message&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;respond_to&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;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&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;message_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;:message&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;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:room_id&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;create_anonymous_user&lt;/span&gt;
    &lt;span class="n"&gt;random_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&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;nickname: &lt;/span&gt;&lt;span class="s2"&gt;"Anonymous_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;random_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="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"new-email-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;random_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@test.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
    &lt;span class="n"&gt;user&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;h4&gt;
  
  
  Views
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Room Index
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container mx-auto px-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Chat Rooms&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="no"&gt;Room&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="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"flex gap-2"&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;f&lt;/span&gt;&lt;span class="o"&gt;|&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;f&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="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"rounded border px-2 py-1"&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="s2"&gt;"Create Room"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-500 text-white px-4 py-1 rounded"&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="k"&gt;end&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;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"rooms"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@rooms&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;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Room Partial
&lt;/h5&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;link_to&lt;/span&gt; &lt;span class="n"&gt;room_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block p-4 border rounded hover:bg-gray-50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;turbo_frame: &lt;/span&gt;&lt;span class="s2"&gt;"_top"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&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;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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="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;h5&gt;
  
  
  Room Show (Chat Interface)
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container mx-auto px-4"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"reset-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="vi"&gt;@room&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;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block mb-4 h-96 overflow-y-auto border rounded p-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;reset_form_target: &lt;/span&gt;&lt;span class="s2"&gt;"messages"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&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;render&lt;/span&gt; &lt;span class="vi"&gt;@messages&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="k"&gt;end&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;turbo_frame_tag&lt;/span&gt; &lt;span class="s2"&gt;"new_message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"_top"&lt;/span&gt; &lt;span class="k"&gt;do&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_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="vi"&gt;@room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"flex gap-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;data: &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;"turbo:submit-end-&amp;gt;reset-form#reset"&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;f&lt;/span&gt;&lt;span class="o"&gt;|&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hidden_field&lt;/span&gt; &lt;span class="ss"&gt;:room_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="vi"&gt;@room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&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;f&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;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"flex-1 rounded border px-2 py-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;reset_form_target: &lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt; &lt;span class="p"&gt;}&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="s2"&gt;"Send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"bg-blue-500 text-white px-4 py-1 rounded"&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="k"&gt;end&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="k"&gt;end&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Message Partial
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"message mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;content_tag&lt;/span&gt; &lt;span class="ss"&gt;:span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"break-words"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  JavaScript
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Reset Form Controller
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollToBottom&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollToBottom&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;scrollToBottom&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messagesTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messagesTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&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;h4&gt;
  
  
  Routes
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:rooms&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:create&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s1"&gt;'rooms#index'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How It All Works Together
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Room Creation and Listing&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can view available rooms on the index page&lt;/li&gt;
&lt;li&gt;Each room is rendered using the &lt;code&gt;_room.html.erb&lt;/code&gt; partial&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Entering a Chat Room&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clicking a room link takes users to the show page&lt;/li&gt;
&lt;li&gt;The show page establishes a Turbo Stream connection&lt;/li&gt;
&lt;li&gt;Existing messages are loaded and displayed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Real-time Message Broadcasting&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a message is created:

&lt;ul&gt;
&lt;li&gt;The form submits to MessagesController#create&lt;/li&gt;
&lt;li&gt;Message is saved to the database&lt;/li&gt;
&lt;li&gt;after_create_commit triggers broadcasting&lt;/li&gt;
&lt;li&gt;All room subscribers receive the update&lt;/li&gt;
&lt;li&gt;New message appears instantly for all users&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Form Handling&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Stimulus controller manages form behavior&lt;/li&gt;
&lt;li&gt;After successful submission, the form is cleared&lt;/li&gt;
&lt;li&gt;The UI remains responsive throughout&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Code In Action
&lt;/h3&gt;

&lt;p&gt;You should see something like this in your browser:&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%2Fgist.github.com%2Fuser-attachments%2Fassets%2Ff6db0d23-7ae8-4774-a10b-0223e0268e39" class="article-body-image-wrapper"&gt;&lt;img alt="image" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgist.github.com%2Fuser-attachments%2Fassets%2Ff6db0d23-7ae8-4774-a10b-0223e0268e39" width="800" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Chat room should be like this:&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%2Fgist.github.com%2Fuser-attachments%2Fassets%2F72f660a3-c09b-4c14-bc59-a8eabf570d52" class="article-body-image-wrapper"&gt;&lt;img alt="image" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgist.github.com%2Fuser-attachments%2Fassets%2F72f660a3-c09b-4c14-bc59-a8eabf570d52" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying the application on Heroku
&lt;/h2&gt;

&lt;p&gt;To deployment on Heroku platform is pretty straighfoward. The prerequisites are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heroku CLI installed&lt;/li&gt;
&lt;li&gt;Git repository initialized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After covering all the prerequisites, let's dive into the steps to the deployment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new Heroku application
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heroku create your-chat-app-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Add the necessary Add-ons
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add Redis add-on&lt;/span&gt;
heroku addons:create heroku-redis:hobby-dev

&lt;span class="c"&gt;# Add PostgreSQL add-on&lt;/span&gt;
heroku addons:create heroku-postgresql:mini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Configure RAILS_MASTER_KEY ENV variable
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heroku config:set &lt;span class="nv"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;config/master.key&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deploy the application
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Push to Heroku&lt;/span&gt;
git push heroku main

&lt;span class="c"&gt;# Run database migrations&lt;/span&gt;
heroku run rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Request a Web Dyno
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heroku ps:scale &lt;span class="nv"&gt;web&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Verify the deployment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check the logs from the deployment process and open the application:&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;# Open the application&lt;/span&gt;
heroku open

&lt;span class="c"&gt;# Monitor logs&lt;/span&gt;
heroku logs &lt;span class="nt"&gt;--tail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why Ruby on Rails is the Best First Framework for New Developers</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Thu, 13 Mar 2025 02:19:44 +0000</pubDate>
      <link>https://dev.to/reinteractive/why-ruby-on-rails-is-the-best-first-framework-for-new-developers-2o7h</link>
      <guid>https://dev.to/reinteractive/why-ruby-on-rails-is-the-best-first-framework-for-new-developers-2o7h</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/best-first-framework-ruby-on-rails" rel="noopener noreferrer"&gt;Rodrigo Souza&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Selecting your first programming language to study is a big choice for all new developers. The menu is huge, and feeling lost is very common. If your objective is to work on web application development, we have a great suggestion for you. Ruby on Rails! It's the best option if you want to build your applications quickly while you learn lots of good programming fundamentals.&lt;/p&gt;

&lt;p&gt;Ruby on Rails is a framework built upon Ruby programming language focused on web application development. It is very easy to learn, readable, and productivity-oriented. Rails provides a well-documented path and the structure that can help you to work on your projects or get your first job as a web applications developer.&lt;/p&gt;

&lt;p&gt;In this article, we will see why development beginners should begin with Ruby on Rails. You will learn how Rails makes programming easy, allows you to build full-stack apps, and gives you job and freelance prospects. So, let's begin!&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Beginner-Friendly Language &amp;amp; Framework
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Readable and Expressive Syntax
&lt;/h4&gt;

&lt;p&gt;Languages like Java or C++ have a complex syntax. This is one of the biggest challenges for beginners, and that's why Ruby shines. Ruby was specially designed to be human readable. In other words, Ruby syntax is very similar to the English language. For example, this is how you run through each item of a list:&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;users&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="nb"&gt;puts&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;name&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 simplicity makes it easier for new developers to focus on problem-solving rather than struggling with syntax.&lt;/p&gt;

&lt;h4&gt;
  
  
  Less Boilerplate Code
&lt;/h4&gt;

&lt;p&gt;Avoiding the usual setup boilerplate, Rails provides built-in features that reduce repetitive configuration tasks. You don't need to write lot's of configuration files or manage dependencies manually. Rails takes care of most of it for you, allowing you to start coding you application.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fast Learning Curve &amp;amp; High Productivity
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Convention Over Configuration (CoC)
&lt;/h4&gt;

&lt;p&gt;Following the "Convention Over Configuration" principle, Rails makes some default assumptions that help beginners to reduce the setup time for some tasks. For example, Rails "automagically" expects a user's table if the application have a Model called User. Pretty cool, no?&lt;/p&gt;

&lt;h4&gt;
  
  
  Don’t Repeat Yourself (DRY)
&lt;/h4&gt;

&lt;p&gt;That is one of the best principles Ruby on Rails encourages. It helps us to write reusable and cleaner code. Rails itself implements it, providing us helper methods, partials and modules that can allow you to efficiently organise your code. It's usable in any language, and you'll learn it using Ruby on Rails.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scaffolding &amp;amp; Generators
&lt;/h4&gt;

&lt;p&gt;When you're learning, getting quick feedback keeps motivation high. Rails makes this possible with scaffolding and generators, which allow you to create entire database-backed applications with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails generate scaffold Post title:string body:text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command generates everything needed for a fully functional CRUD (Create, Read, Update, Delete) interface, giving beginners an instant hands-on experience with web development.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Full-Stack Web Development in One Framework
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Covers Backend &amp;amp; Frontend
&lt;/h4&gt;

&lt;p&gt;Rails is a full-stack framework, meaning you can build an entire web application using just Rails. You’ll learn how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle client requests and routing&lt;/li&gt;
&lt;li&gt;Store and retrieve data from a database&lt;/li&gt;
&lt;li&gt;Render an HTML page using (or not) Javascript&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Active Record (ORM) – Simplified Database Management
&lt;/h4&gt;

&lt;p&gt;In order to make database interactions simple, Rails brings an Object-Relational Mapping (ORM) tool. It's called Active Record. It's a huge library (Gem) that can write complex SQL queries for you while you just code something like this:&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;User&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="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;The SQL version will be as follows:&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="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easier to understand and work with databases.&lt;/p&gt;

&lt;h4&gt;
  
  
  Built-in Testing Tools
&lt;/h4&gt;

&lt;p&gt;In a very competitive world, where all the tasks should be delivered faster, build automatic tests accurately is a crucial step to have a reliable application. Rails includes built-in support for unit testing and system testing, helping you to adopt the best practices early on.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Strong &amp;amp; Supportive Community
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Well-Established Ecosystem
&lt;/h4&gt;

&lt;p&gt;One of the most incredible things about Ruby on Rails is the ecosystem. Ruby on Rails has been around since 2004 and has a mature ecosystem with thousands of gems (plugins) available. The options range from authentication to an entire DSL for API integrations. Of course, it saves time and effort.&lt;/p&gt;

&lt;h4&gt;
  
  
  Helpful Community &amp;amp; Mentorship
&lt;/h4&gt;

&lt;p&gt;New developers are welcome in the Rails community. There are tons of free tutorials, guides, and discussion forums where you can ask for help and get support. Lots of experienced developers actively mentor newcomers, making it easier to learn.&lt;/p&gt;

&lt;h4&gt;
  
  
  Contributing to Open Source
&lt;/h4&gt;

&lt;p&gt;In the Ruby on Rails community, we have many open-source projects that allow the new developer to contribute to real-world applications, learn from others, and gain experience that will help with job opportunities.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Great for Building Real-World Projects
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Popular for Startups
&lt;/h4&gt;

&lt;p&gt;Several successful startups, including GitHub, Shopify, Airbnb, and Basecamp, built their products on Rails. If you wish to create your own startup or side projects, Rails is a perfect tool because it helps you build and deploy applications instantly.&lt;/p&gt;

&lt;h4&gt;
  
  
  Fast Prototyping
&lt;/h4&gt;

&lt;p&gt;Rails allows you to go from idea to working prototype in days, not weeks or months. It is perfect for new developers who want to quickly experiment with their ideas without having to spend much time on installing their environment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Used by Big Companies
&lt;/h4&gt;

&lt;p&gt;Even with newer frameworks coming out every day, Ruby on Rails is still utilized by numerous big companies. Learning Ruby on Rails framework gears you up for actual development, making it more likely for you to be hired.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Job Opportunities &amp;amp; Freelancing Potential
&lt;/h3&gt;

&lt;h4&gt;
  
  
  High Demand in Web Development
&lt;/h4&gt;

&lt;p&gt;Even though new technologies emerge all the time, Rails is still widely used in web development, especially among startups. Many companies look for junior Rails developers, making it a great first step toward employment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Freelancing &amp;amp; Side Projects
&lt;/h4&gt;

&lt;p&gt;Rails is an excellent choice for freelancers because you can build entire applications on your own without needing a team. This allows you to offer custom web development services and work on personal projects without external dependencies.&lt;/p&gt;

&lt;h4&gt;
  
  
  Great Foundation for Learning Other Languages
&lt;/h4&gt;

&lt;p&gt;Rails teaches core programming concepts like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MVC (Model-View-Controller) – Used in many frameworks (Django, Laravel, ASP.NET).&lt;/li&gt;
&lt;li&gt;RESTful APIs – A standard in web development.&lt;/li&gt;
&lt;li&gt;Object-Oriented Programming (OOP) is a foundation for many modern languages.&lt;/li&gt;
&lt;li&gt;Once you master Rails, transitioning to languages like Python, JavaScript, or Elixir will be much easier.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;As you can see, Ruby on Rails is one of the best choices for new developers because it aligns a friendly learning experience and a supportive community with a super productive development process. Avoiding the complexities of web development, you will learn many important programming concepts that you will bring to you benefits idependent of the language you choose next.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Hash Replacement with `sub` and `gsub` in Ruby on Rails</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Tue, 04 Mar 2025 11:39:03 +0000</pubDate>
      <link>https://dev.to/reinteractive/hash-replacement-with-sub-and-gsub-in-ruby-on-rails-18nc</link>
      <guid>https://dev.to/reinteractive/hash-replacement-with-sub-and-gsub-in-ruby-on-rails-18nc</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/rails-ruby-hash-replacement-sub-gsub" rel="noopener noreferrer"&gt;Suman Awal&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sub/gsub&lt;/code&gt; is the widely used substitution method in ruby. These methods replace (substitute) content of the string with the &lt;br&gt;
new string based on the provided logic. In SAAS application, we offen encounter the condition where we need to generate dynamic content for a single action based on the customer. For example generating a dynamic welcome message to the customer for the different client. There are lots of ways to get the result however in this article we will use the one of the mostly used ruby method &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;gsub&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;gsub&lt;/code&gt; ruby methods
&lt;/h3&gt;

&lt;p&gt;Before we get started, let's understand what &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;gsub&lt;/code&gt; do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt;: Replaces the &lt;strong&gt;first&lt;/strong&gt; occurrence of &lt;code&gt;pattern&lt;/code&gt; in a string with &lt;code&gt;replacement string&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gsub&lt;/code&gt;: Replaces &lt;strong&gt;all&lt;/strong&gt; occurrences of &lt;code&gt;pattern&lt;/code&gt; in a string with &lt;code&gt;replacement string&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both methods use a regular expression as the &lt;code&gt;pattern&lt;/code&gt; and a string or a block as the &lt;code&gt;replacement&lt;/code&gt;. Here we will explain using a block (hash) for dynamic replacement based on our hash.&lt;/p&gt;

&lt;p&gt;Here's a simple example:&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;replacements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Glenn Maxwell'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'country'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Australia'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hi, my name is {{name}} and I am from {{country}}."&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&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="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;replacements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="vg"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="c1"&gt;# Output: "Hi, my name is Glenn Maxwell and I am from Australia"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We define a &lt;code&gt;replacements&lt;/code&gt; hash containing the key-value pairs we want to use for the replacement in the string.&lt;/li&gt;
&lt;li&gt; We define a &lt;code&gt;template&lt;/code&gt; string containing placeholders enclosed in double curly braces (&lt;code&gt;{{}}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; We use &lt;code&gt;gsub&lt;/code&gt; with the regular expression &lt;code&gt;/{{(.*?)}}/&lt;/code&gt; to find all occurrences of these placeholders.&lt;/li&gt;
&lt;li&gt; The block is executed for each match. Inside the block:&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;sub&lt;/code&gt; for Single Replacements
&lt;/h3&gt;

&lt;p&gt;If you only need to replace the first occurrence of a pattern, you can use &lt;code&gt;sub&lt;/code&gt; instead of &lt;code&gt;gsub&lt;/code&gt;. The logic remains the same.&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;replacements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Glenn Maxwell'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hi, my name is {{name}} and my friend's name is also {{name}}."&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&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="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;replacements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="vg"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# Output: Hi, my name is Glenn Maxwell and my friend's name is also {{name}}.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Real-World Rails Examples
&lt;/h3&gt;

&lt;p&gt;This technique is useful in various Rails scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generate dynamic emails:&lt;/strong&gt; You can store email templates with placeholders in your database and replace them with user-specific data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create dynamic reports:&lt;/strong&gt;  Generate reports with data pulled from various sources, using a hash to map placeholders to the correct values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localize content:&lt;/strong&gt;  Store localized strings in a hash and replace placeholders in your views based on the user's locale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here you can find one of the widely used example to &lt;strong&gt;Generate dynamic emails&lt;/strong&gt; for the SAAS application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate dynamic emails using hash replacement
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Scenario
&lt;/h4&gt;

&lt;p&gt;You have a Rails application that serves multiple clients. Each client has their own set of customers. When a new customer registers for a specific client, the application sends a welcome email. The content of the welcome email is dynamically generated based on a template stored in the database, which is specific to each client.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sample codes
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Create models
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# app/models/client.rb&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Client&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;:customers&lt;/span&gt;
    &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="ss"&gt;:welcome_email_template&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# app/models/customer.rb&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&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;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:client&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# app/models/welcome_email_template.rb&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WelcomeEmailTemplate&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;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:client&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Migrations
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# db/migrate/xxxxxx_create_clients.rb&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateClients&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;create_table&lt;/span&gt; &lt;span class="ss"&gt;:clients&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="c1"&gt;# For identifying clients (e.g., client1.example.com)&lt;/span&gt;

        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;end&lt;/span&gt;

  &lt;span class="c1"&gt;# db/migrate/xxxxxx_create_customers.rb&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateCustomers&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;create_table&lt;/span&gt; &lt;span class="ss"&gt;:customers&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;end&lt;/span&gt;

  &lt;span class="c1"&gt;# db/migrate/xxxxxx_create_welcome_email_templates.rb&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateWelcomeEmailTemplates&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;create_table&lt;/span&gt; &lt;span class="ss"&gt;:welcome_email_templates&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;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="ss"&gt;:template&lt;/span&gt; &lt;span class="c1"&gt;# The email template with placeholders&lt;/span&gt;

        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Database seed
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# db/seeds.rb&lt;/span&gt;
&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy_all&lt;/span&gt;
&lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy_all&lt;/span&gt;
&lt;span class="no"&gt;WelcomeEmailTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy_all&lt;/span&gt;

&lt;span class="n"&gt;client1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Client&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="s1"&gt;'Client One'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;subdomain: &lt;/span&gt;&lt;span class="s1"&gt;'client1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Client&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="s1"&gt;'Client Two'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;subdomain: &lt;/span&gt;&lt;span class="s1"&gt;'client2'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;WelcomeEmailTemplate&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;client: &lt;/span&gt;&lt;span class="n"&gt;client1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;template: &lt;/span&gt;&lt;span class="s2"&gt;"Welcome, {{customer_name}}!&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Thank you for joining Client One. Your account has been created.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Best regards,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;The Client One Team"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;WelcomeEmailTemplate&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;client: &lt;/span&gt;&lt;span class="n"&gt;client2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;template: &lt;/span&gt;&lt;span class="s2"&gt;"Hello {{customer_name}},&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Welcome to Client Two! We're excited to have you on board.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Sincerely,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;The Client Two Team"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Customer Registration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/customers_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomersController&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="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_client&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;@customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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;@customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_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;@customer&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;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@customer&lt;/span&gt;&lt;span class="p"&gt;)&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;'Customer 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="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;set_client&lt;/span&gt;
    &lt;span class="c1"&gt;# Assumes you have a way to identify the client, e.g., via subdomain&lt;/span&gt;
    &lt;span class="vi"&gt;@client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Client&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;subdomain: &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@client&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;plain: &lt;/span&gt;&lt;span class="s2"&gt;"Client not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :not_found&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;def&lt;/span&gt; &lt;span class="nf"&gt;customer_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;:customer&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;:email&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="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;welcome_email_template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;template&lt;/span&gt;
    &lt;span class="n"&gt;welcome_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_welcome_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;CustomerMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;welcome_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;welcome_message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&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;generate_welcome_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;replacements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s1"&gt;'customer_name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&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="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;replacements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="vg"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;match&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;ul&gt;
&lt;li&gt;Routes
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;subdomain: &lt;/span&gt;&lt;span class="s1"&gt;'client1'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;module: &lt;/span&gt;&lt;span class="s1"&gt;'client1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'client1'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:create&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="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;subdomain: &lt;/span&gt;&lt;span class="s1"&gt;'client2'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;module: &lt;/span&gt;&lt;span class="s1"&gt;'client2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'client2'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:create&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="c1"&gt;# Non-subdomain routes (e.g., for admin panel)&lt;/span&gt;
&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:clients&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example provides a basic application for handling multiple clients with customized welcome messages for their customer.&lt;/p&gt;

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

&lt;p&gt;Using &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;gsub&lt;/code&gt; with hash replacement provides a flexible and efficient way to dynamically generate string and can be used in real application.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Imagine: Rails 8 Solid Trifecta: Cache, Cable, Queue</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Tue, 18 Feb 2025 06:23:30 +0000</pubDate>
      <link>https://dev.to/reinteractive/imagine-rails-8-solid-trifecta-cache-cable-queue-45de</link>
      <guid>https://dev.to/reinteractive/imagine-rails-8-solid-trifecta-cache-cable-queue-45de</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/rails-8-solid-trifecta-comparison-cache-cable-queue" rel="noopener noreferrer"&gt;Allan Andal&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Rails 8 - Solid Trifecta Comparison
&lt;/h1&gt;

&lt;p&gt;The "Solid Trifecta" is a suite of database-backed solutions—&lt;strong&gt;Solid Cache&lt;/strong&gt;, &lt;strong&gt;Solid Cable&lt;/strong&gt;, and &lt;strong&gt;Solid Queue&lt;/strong&gt;—added in Ruby on Rails 8 to simplify application architecture by reducing the need for external services like Redis and Memcached. These components are built on top of existing database infrastructure to handle caching, WebSocket messaging, and background job processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solid Cache
&lt;/h2&gt;

&lt;p&gt;Traditional RAM-based caching systems are replaced by Solid Cache which uses disk storage, including SSDs and NVMe drives. This method provides bigger cache storage at lower costs which leads to extended cache retention times and enhanced application performance. As an example, Basecamp has adopted Solid Cache to store 10 terabytes of data with a 60-day retention window, which has resulted in a significant reduction in render times.&lt;/p&gt;

&lt;h4&gt;
  
  
  Comparison: Solid Cache vs. Dalli/Memcached vs. Redis
&lt;/h4&gt;

&lt;p&gt;Overview&lt;/p&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;Solid Cache&lt;/th&gt;
&lt;th&gt;Dalli + Memcached&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Disk-based (local SSD)&lt;/td&gt;
&lt;td&gt;In-memory (RAM)&lt;/td&gt;
&lt;td&gt;In-memory (RAM) + optional persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (disk-based, survives restarts)&lt;/td&gt;
&lt;td&gt;No (data lost on restart)&lt;/td&gt;
&lt;td&gt;Yes (via RDB &amp;amp; AOF)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scales with disk size&lt;/td&gt;
&lt;td&gt;Scales with RAM&lt;/td&gt;
&lt;td&gt;Scales with RAM, supports clustering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slower (disk access)&lt;/td&gt;
&lt;td&gt;Very fast&lt;/td&gt;
&lt;td&gt;Very fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Good for multi-threaded apps&lt;/td&gt;
&lt;td&gt;High concurrency&lt;/td&gt;
&lt;td&gt;High concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Structures&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Key-value store only&lt;/td&gt;
&lt;td&gt;Key-value store only&lt;/td&gt;
&lt;td&gt;Supports lists, hashes, sets, sorted sets, streams, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apps needing persistence and local caching&lt;/td&gt;
&lt;td&gt;High-speed caching across multiple servers&lt;/td&gt;
&lt;td&gt;Caching, real-time analytics, session storage, message queues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Performance &amp;amp; Scalability&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memcached&lt;/strong&gt; and &lt;strong&gt;Redis&lt;/strong&gt; are &lt;strong&gt;much faster&lt;/strong&gt; than &lt;strong&gt;Solid Cache&lt;/strong&gt; since they store data &lt;strong&gt;in-memory&lt;/strong&gt; rather than on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memcached&lt;/strong&gt; is &lt;strong&gt;simpler&lt;/strong&gt; and optimized for &lt;strong&gt;high-speed key-value lookups&lt;/strong&gt;, but it lacks persistence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; can be &lt;strong&gt;persistent&lt;/strong&gt; (with RDB or AOF) and supports &lt;strong&gt;advanced data types&lt;/strong&gt;, making it more versatile than Memcached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solid Cache&lt;/strong&gt; is &lt;strong&gt;slower&lt;/strong&gt; but allows for &lt;strong&gt;larger cache sizes&lt;/strong&gt; (limited by disk, not RAM).&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Use Cases&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Solid Cache&lt;/th&gt;
&lt;th&gt;Dalli + Memcached&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Persistent caching&lt;/strong&gt; (survives restart)&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Distributed caching&lt;/strong&gt; (multi-server)&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fastest performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No (disk I/O)&lt;/td&gt;
&lt;td&gt;✅ Yes (RAM)&lt;/td&gt;
&lt;td&gt;✅ Yes (RAM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Large dataset caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes (limited by disk)&lt;/td&gt;
&lt;td&gt;❌ No (limited by RAM)&lt;/td&gt;
&lt;td&gt;✅ Yes (with clustering)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Message queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes (Pub/Sub, Streams)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complex data structures&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes (lists, sets, sorted sets, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Ease of Use &amp;amp; Setup&lt;/p&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;Solid Cache&lt;/th&gt;
&lt;th&gt;Dalli + Memcached&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup Simplicity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Easiest (built-in Rails cache store)&lt;/td&gt;
&lt;td&gt;❌ Requires Memcached server&lt;/td&gt;
&lt;td&gt;❌ Requires Redis server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration with Rails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes (out-of-the-box)&lt;/td&gt;
&lt;td&gt;✅ Yes (via &lt;code&gt;dalli&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ Yes (via &lt;code&gt;redis-rails&lt;/code&gt;, supports up to Rails 7 only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance Overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Low&lt;/td&gt;
&lt;td&gt;✅ Low&lt;/td&gt;
&lt;td&gt;❌ Higher (needs persistence configuration &amp;amp; monitoring)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Conclusion: Which is Best?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Solid Cache&lt;/strong&gt; if you need &lt;strong&gt;persistence, local caching, and simple Rails integration&lt;/strong&gt;, but can tolerate slower performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Memcached&lt;/strong&gt; if you need &lt;strong&gt;the fastest possible caching performance&lt;/strong&gt; and &lt;strong&gt;don’t need persistence&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Redis&lt;/strong&gt; if you need &lt;strong&gt;fast caching plus advanced features&lt;/strong&gt; (persistence, pub/sub, sorted sets, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 &lt;strong&gt;Best overall caching solution?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you only need simple, fast caching → &lt;strong&gt;Memcached&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;If you need &lt;strong&gt;caching + persistence + advanced features&lt;/strong&gt; → &lt;strong&gt;Redis&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;If you want &lt;strong&gt;a simple Rails-native cache that persists to disk&lt;/strong&gt; → &lt;strong&gt;Solid Cache&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solid Cable
&lt;/h2&gt;

&lt;p&gt;Solid Cable acts as a database-backed option to manage WebSocket connections so applications do not need an additional pub/sub server such as Redis. The system sends messages between application processes and clients using fast polling methods which support near real-time performance. The database stores messages for at least a day which helps developers review  live update history. &lt;/p&gt;

&lt;h4&gt;
  
  
  Comparison: Solid Cable vs. AnyCable vs. Action Cable
&lt;/h4&gt;

&lt;p&gt;Overview&lt;/p&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;Solid Cable&lt;/th&gt;
&lt;th&gt;AnyCable&lt;/th&gt;
&lt;th&gt;Action Cable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slower than Action Cable&lt;/td&gt;
&lt;td&gt;Fastest (gRPC/WebSockets)&lt;/td&gt;
&lt;td&gt;Fast (in-memory, event-driven)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slower uses database polling&lt;/td&gt;
&lt;td&gt;Very High (gRPC &amp;amp; WebSockets)&lt;/td&gt;
&lt;td&gt;Uses threads (Puma) and Redis Pub/Sub for real-time message broadcasting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sacles with database&lt;/td&gt;
&lt;td&gt;Best for large-scale apps&lt;/td&gt;
&lt;td&gt;Scales well with Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (database)&lt;/td&gt;
&lt;td&gt;No (in-memory)&lt;/td&gt;
&lt;td&gt;No (in-memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower-traffic apps&lt;/td&gt;
&lt;td&gt;Large-scale, distributed WebSockets&lt;/td&gt;
&lt;td&gt;High-traffic apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Performance &amp;amp; Scalability&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AnyCable&lt;/strong&gt; is &lt;strong&gt;the fastest&lt;/strong&gt; since it offloads WebSocket handling to a separate &lt;strong&gt;gRPC server&lt;/strong&gt; (often using Golang).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Cable&lt;/strong&gt; is &lt;strong&gt;faster&lt;/strong&gt; because it multi-threaded, even-driven.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solid Cable&lt;/strong&gt; is &lt;strong&gt;slowest&lt;/strong&gt; because of DB read/write and database operation however it is the simpliest setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scalability&lt;/p&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;Solid Cable&lt;/th&gt;
&lt;th&gt;AnyCable&lt;/th&gt;
&lt;th&gt;Action Cable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-server scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes (database)&lt;/td&gt;
&lt;td&gt;✅ Yes (Redis)&lt;/td&gt;
&lt;td&gt;✅ Yes (Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Ease of Use &amp;amp; Setup&lt;/p&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;Solid Cable&lt;/th&gt;
&lt;th&gt;AnyCable&lt;/th&gt;
&lt;th&gt;Action Cable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Easy (drop-in replacement for Action Cable)&lt;/td&gt;
&lt;td&gt;❌ Complex (requires AnyCable server)&lt;/td&gt;
&lt;td&gt;✅ Easy (built into Rails)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration with Rails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Requires extra infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes (gRPC server)&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works with existing Action Cable code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Conclusion: Which is Best?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use AnyCable&lt;/strong&gt; if you need &lt;strong&gt;massive scalability&lt;/strong&gt; and &lt;strong&gt;low-latency WebSockets&lt;/strong&gt; across multiple servers.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Action Cable&lt;/strong&gt; if you need &lt;strong&gt;better performance than Solid Cable&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Solid Cable&lt;/strong&gt; if you just need &lt;strong&gt;basic real-time updates&lt;/strong&gt; and want the simplest setup.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 &lt;strong&gt;Best overall WebSockets solution?&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you want &lt;strong&gt;basic real-time features for a small Rails app without extra infrastructure&lt;/strong&gt; → &lt;strong&gt;Solid Cable&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;If you need &lt;strong&gt;WebSockets at scale with thousands of connections&lt;/strong&gt; → &lt;strong&gt;AnyCable&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Otherwise → &lt;strong&gt;Action Cable&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solid Queue
&lt;/h2&gt;

&lt;p&gt;For background job processing Solid Queue presents a database-driven solution that makes Sidekiq  and Resque external job runners unnecessary. Solid Queue utilizes database features such as &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; to efficiently manage job queues. This service can function as a Puma plugin as well as through a  dedicated dispatcher giving users flexible job management capabilities.&lt;/p&gt;

&lt;h4&gt;
  
  
  Comparison: Solid Queue vs. Sidekiq vs. Resque
&lt;/h4&gt;

&lt;p&gt;Overview&lt;/p&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;Solid Queue&lt;/th&gt;
&lt;th&gt;Sidekiq&lt;/th&gt;
&lt;th&gt;Resque&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (multi-threaded)&lt;/td&gt;
&lt;td&gt;High (multi-threaded)&lt;/td&gt;
&lt;td&gt;Moderate (single-threaded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (multi-threaded)&lt;/td&gt;
&lt;td&gt;Very High (multi-threaded)&lt;/td&gt;
&lt;td&gt;Moderate (single-threaded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scales well (single server)&lt;/td&gt;
&lt;td&gt;Excellent (distributed)&lt;/td&gt;
&lt;td&gt;Good (distributed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (supports Redis)&lt;/td&gt;
&lt;td&gt;Yes (supports Redis)&lt;/td&gt;
&lt;td&gt;Yes (supports Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (configurable)&lt;/td&gt;
&lt;td&gt;Yes (configurable)&lt;/td&gt;
&lt;td&gt;Yes (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Job Prioritization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High-performance background jobs&lt;/td&gt;
&lt;td&gt;High-concurrency, real-time jobs&lt;/td&gt;
&lt;td&gt;Simple, reliable job processing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Performance &amp;amp; Scalability&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solid Queue&lt;/strong&gt; is &lt;strong&gt;built for performance&lt;/strong&gt; with Ruby’s async model, making it &lt;strong&gt;faster&lt;/strong&gt; than &lt;strong&gt;Resque&lt;/strong&gt; (single-threaded) and comparable to &lt;strong&gt;Sidekiq&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidekiq&lt;/strong&gt; is known for its &lt;strong&gt;high concurrency&lt;/strong&gt; with &lt;strong&gt;multi-threading&lt;/strong&gt;, handling jobs in parallel across multiple threads, which makes it &lt;strong&gt;extremely fast&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resque&lt;/strong&gt; uses a &lt;strong&gt;single-threaded model&lt;/strong&gt; (in Ruby), meaning it's &lt;strong&gt;slower&lt;/strong&gt; than both &lt;strong&gt;Sidekiq&lt;/strong&gt; and &lt;strong&gt;Solid Queue&lt;/strong&gt; for high-volume workloads.&lt;/li&gt;
&lt;/ul&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;Solid Queue&lt;/th&gt;
&lt;th&gt;Sidekiq&lt;/th&gt;
&lt;th&gt;Resque&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-server scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pub/Sub support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External adapters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Database&lt;/td&gt;
&lt;td&gt;✅ Redis&lt;/td&gt;
&lt;td&gt;✅ Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Job retry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (async)&lt;/td&gt;
&lt;td&gt;Very high (multi-threaded)&lt;/td&gt;
&lt;td&gt;Low (single-threaded)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Use Cases&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Solid Queue&lt;/th&gt;
&lt;th&gt;Sidekiq&lt;/th&gt;
&lt;th&gt;Resque&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple background job processing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High concurrency, real-time background jobs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Large-scale job processing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Job prioritization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue with multiple workers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Handling thousands of jobs per second&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Ease of Use &amp;amp; Setup&lt;/p&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;Solid Queue&lt;/th&gt;
&lt;th&gt;Sidekiq&lt;/th&gt;
&lt;th&gt;Resque&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Easy (out-of-the-box integration)&lt;/td&gt;
&lt;td&gt;✅ Moderate (setup Redis and multi-threading)&lt;/td&gt;
&lt;td&gt;✅ Moderate (setup Redis and single-threaded workers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration with Rails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Requires extra infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No (use existing database)&lt;/td&gt;
&lt;td&gt;✅ Yes (Redis, multi-threading setup)&lt;/td&gt;
&lt;td&gt;✅ Yes (Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Job Management UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No (CLI-based)&lt;/td&gt;
&lt;td&gt;✅ Yes (web UI for monitoring)&lt;/td&gt;
&lt;td&gt;✅ Yes (web UI for monitoring)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Conclusion: Which is Best?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Solid Queue&lt;/strong&gt; if you need &lt;strong&gt;high-performance background job processing&lt;/strong&gt; with &lt;strong&gt;single-server scaling&lt;/strong&gt; and can benefit from &lt;strong&gt;Ruby-based async performance&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Sidekiq&lt;/strong&gt; if you need &lt;strong&gt;multi-threaded, high-concurrency job processing&lt;/strong&gt; with &lt;strong&gt;real-time capabilities&lt;/strong&gt; and &lt;strong&gt;distributed scaling&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Resque&lt;/strong&gt; if you need &lt;strong&gt;simple background job processing&lt;/strong&gt; and don’t need high concurrency or performance optimization, but still want reliability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 &lt;strong&gt;Best overall background job processor?&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you need &lt;strong&gt;high-performance job processing with Ruby&lt;/strong&gt; → &lt;strong&gt;Solid Queue&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;If you need &lt;strong&gt;extreme concurrency and distributed job processing&lt;/strong&gt; → &lt;strong&gt;Sidekiq&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;If you need &lt;strong&gt;a simple, reliable queue system&lt;/strong&gt; with less focus on performance → &lt;strong&gt;Resque&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Can the Solid Trifecta replace Redis/Memcached?
&lt;/h2&gt;

&lt;p&gt;The Solid Trifecta replaces external services like Redis and  Memcached through its built-in database-driven solutions. Most applications that require moderate performance levels will find these  integrated solutions suitable because they provide necessary features without increasing deployment complexity. Traditional solutions remain preferable for applications with  demanding performance requirements because Redis and Memcached deliver optimized functionality.&lt;/p&gt;

&lt;p&gt;Rails 8's Solid Trifecta  enables developers to build more streamlined systems when caching messaging and job processing by utilizing existing database systems. These  solutions provide many benefits for simplicity and cost-effectiveness but application users &lt;strong&gt;should evaluate their particular needs&lt;/strong&gt; to decide if they can substitute traditional services before choosing Redis or Memcached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;New apps should start with &lt;strong&gt;fewer dependencies&lt;/strong&gt; using Rails 8’s built in solutions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling for performance&lt;/strong&gt;: To keep costs low, use specialized tools only for higher speed requirements as the app scales.&lt;/p&gt;

&lt;p&gt;Large applications should continue using high performing tools like Sidekiq for job processing along with Memcached for caching where required.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Revisiting Performance in Ruby 3.4.1</title>
      <dc:creator>reinteractive</dc:creator>
      <pubDate>Wed, 12 Feb 2025 07:48:01 +0000</pubDate>
      <link>https://dev.to/reinteractive/revisiting-performance-in-ruby-341-2nee</link>
      <guid>https://dev.to/reinteractive/revisiting-performance-in-ruby-341-2nee</guid>
      <description>&lt;p&gt;Credited to: &lt;a href="https://reinteractive.com/articles/tutorial-series-for-experienced-rails-developers/ruby-3-4-1-struct-class-performance" rel="noopener noreferrer"&gt;Miko Dagatan&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Before, there are few articles that rose up saying that in terms of performance, &lt;code&gt;Struct&lt;/code&gt;s are powerful and could be used to define some of the code in place of the &lt;code&gt;Class&lt;/code&gt;. Two of these are &lt;a href="https://alchemists.io/articles/ruby_structs" rel="noopener noreferrer"&gt;this one&lt;/a&gt; and &lt;a href="https://thiagolimadeveloper.medium.com/structs-in-ruby-b22c99e07208" rel="noopener noreferrer"&gt;this one&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's revisit these things with the latest Ruby version, 3.4.1, so that we can see whether this perspective still holds true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code for Benchmarking
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class BenchmarkHashStruct
  class &amp;lt;&amp;lt; self

    NUM = 1_000_000

    def measure
      array
      hash_str
      hash_sym
      klass
      struct
      data
    end

    def new_class
      @class ||= Class.new do
        attr_reader :name
        def initialize(name:)
          @name = name
        end
      end
    end

    def array
      time = Benchmark.measure do
        NUM.times do
          array = [Faker.name]
          hash[0]
        end
      end

      puts "array: #{time}" 
    end

    def hash_str
      time = Benchmark.measure do
        NUM.times do
          hash = { 'name' =&amp;gt; Faker.name }
          hash['name']
        end
      end

      puts "hash_str: #{time}" 
    end

    def hash_sym
      time = Benchmark.measure do
        NUM.times do
          hash = { name: Faker.name }
          hash[:name]
        end
      end

      puts "hash_sym: #{time}" 
    end

    def struct
      time = Benchmark.measure do
        struct = Struct.new(:name) # Structs are only initialized once especially for large datasets
        NUM.times do |i|
          init = struct.new(name: Faker.name)
          init.name
        end

      end
      puts "struct: #{time}"
    end

    def klass
      time = Benchmark.measure do
        klass = new_class
        NUM.times do
          a = klass.new(name: Faker.name)
          a.name
        end
      end

      puts "class: #{time}"
    end

    def data
      time = Benchmark.measure do
        name_data = Data.define(:name)
        NUM.times do
          a = name_data.new(name: Faker.name)
          a.name
        end
      end

      puts "data: #{time}"
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Explanation
&lt;/h2&gt;

&lt;p&gt;In this file, we're simply trying to create benchmark measures for arrays, hashes with string keys, hashes with symbolized keys, structs, classes, and data. In a the lifetime of these objects, we understand that we instantiate them then we access the data we stored. So, we'll simulate only that for our tests. We use 1 million instances of these scenarios and see the results. The &lt;code&gt;measure&lt;/code&gt; method will show all of these measurements together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;performance(dev)&amp;gt; BenchmarkHashStruct.measure
array:   0.124267   0.000000   0.124267 (  0.129573)
hash_str:   0.264137   0.000000   0.264137 (  0.275421)
hash_sym:   0.174082   0.000000   0.174082 (  0.181514)
class:   0.308020   0.000000   0.308020 (  0.321165)
struct:   0.336229   0.000000   0.336229 (  0.350576)
data:   0.345480   0.000000   0.345480 (  0.360232)
=&amp;gt; nil

performance(dev)&amp;gt; BenchmarkHashStruct.measure
array:   0.090669   0.000378   0.091047 (  0.094786)
hash_str:   0.264261   0.000000   0.264261 (  0.275104)
hash_sym:   0.172333   0.000000   0.172333 (  0.179407)
class:   0.311545   0.000060   0.311605 (  0.324390)
struct:   0.335436   0.000000   0.335436 (  0.349203)
data:   0.346124   0.000071   0.346195 (  0.360396)
=&amp;gt; nil

performance(dev)&amp;gt; BenchmarkHashStruct.measure
array:   0.088372   0.003872   0.092244 (  0.096181)
hash_str:   0.265748   0.000464   0.266212 (  0.277565)
hash_sym:   0.174393   0.000000   0.174393 (  0.181831)
class:   0.309411   0.000000   0.309411 (  0.322613)
struct:   0.346008   0.000000   0.346008 (  0.360760)
data:   0.344666   0.000000   0.344666 (  0.359361)
=&amp;gt; nil

performance(dev)&amp;gt; BenchmarkHashStruct.measure
array:   0.077396   0.000038   0.077434 (  0.080771)
hash_str:   0.242372   0.000140   0.242512 (  0.252853)
hash_sym:   0.159206   0.000000   0.159206 (  0.166007)
class:   0.273878   0.009250   0.283128 (  0.295201)
struct:   0.322791   0.000323   0.323114 (  0.336889)
data:   0.346099   0.000038   0.346137 (  0.360901)
=&amp;gt; nil
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've run &lt;code&gt;measure&lt;/code&gt; 4 times to account for any random changes that may have come and completely ensure of the performance of these tests. As expected, we see array at the top while symbolized hashes goes as a general second. We see that stringified hashes falls at the 3rd, with a huge gap when compared the the symbolized hashes. Then, when we look at class vs structs, it seems that structs have fallen a little bit behind compared to the classes. We could surmise that there is probably a performance boost done to classes in the recent patches. &lt;/p&gt;

&lt;p&gt;Also, we could see that the Data object that was introduced in Ruby 3.2.0+ was falling behind the Struct object. This may be problematic since the Data object is basically a Struct that is immutable, so there's already disadvantages of using Data over Struct. We may still prefer Struct over Data considering that there's a bit of a performance bump over the Data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There are 2 takeaways from this test. First, it's really important that we use symbolized hashes over stringified hashes as the former 1.5x faster than the latter. Meanwhile, if not using hashes, it's better to use Classes over Structs, unlike what was previously encouraged. Classes are now 1.07x - 1.14x times faster than structs, so it's encouraged to keep using them. &lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
