<?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: lef237</title>
    <description>The latest articles on DEV Community by lef237 (@lef237).</description>
    <link>https://dev.to/lef237</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3432083%2Fa7f2ce66-61e8-43cc-b1b6-e87e67fdf0e2.jpg</url>
      <title>DEV Community: lef237</title>
      <link>https://dev.to/lef237</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lef237"/>
    <language>en</language>
    <item>
      <title>Use gst to safely get an overview of all git states</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Thu, 04 Jun 2026 11:56:32 +0000</pubDate>
      <link>https://dev.to/lef237/use-gst-for-an-overview-of-every-git-state-35km</link>
      <guid>https://dev.to/lef237/use-gst-for-an-overview-of-every-git-state-35km</guid>
      <description>&lt;p&gt;Checking changed files with &lt;code&gt;git status&lt;/code&gt;, looking at history with &lt;code&gt;git log&lt;/code&gt;, checking diffs with &lt;code&gt;git diff&lt;/code&gt;...&lt;/p&gt;

&lt;p&gt;When you repeatedly jump back and forth between these commands while working, you sometimes start thinking, “It would be nice if I could see all the information I want right now in one place.”&lt;/p&gt;

&lt;p&gt;So I created gst, a CLI tool that lets you check the state of a Git repository at a glance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lef237/gst" rel="noopener noreferrer"&gt;lef237/gst: Read-only Git status visualizer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I was teaching Git to an acquaintance who had just started programming, and they said things like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I don’t really understand git status&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It’s hard to remember so many different commands&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’ve heard of tools like lazygit, but I’m scared I might accidentally change the repository state&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That made me think: why not build a tool that solves those problems?&lt;/p&gt;

&lt;p&gt;That was the starting point for this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can See
&lt;/h2&gt;

&lt;p&gt;When you launch &lt;code&gt;gst&lt;/code&gt;, you can view your current branch, differences from remote repositories, modified files, staged changes, working directory diffs, stashes, and the commit graph—all from your terminal.&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%2Fcoeqerrp7bnikc1ynu7d.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%2Fcoeqerrp7bnikc1ynu7d.png" alt="gst overview" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo video
&lt;/h2&gt;

&lt;p&gt;A picture is worth a thousand words, so we have also prepared a video demonstration.&lt;/p&gt;

&lt;p&gt;Watching it will give you a better sense of how &lt;code&gt;gst&lt;/code&gt; works in practice and why it can be useful.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/EMO3DaNkqT0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;There are two ways to install it.&lt;/p&gt;

&lt;p&gt;If you use Go, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/lef237/gst/cmd/gst@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It also supports Homebrew. You can install it with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;lef237/tap/gst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Normally, run the following command inside a repository:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you want to display the information once and then exit, use --once:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gst &lt;span class="nt"&gt;--once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In the TUI, you can move between views using the Tab key and arrow keys.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;r&lt;/code&gt; to reload, and &lt;code&gt;q&lt;/code&gt; to quit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read-Only
&lt;/h2&gt;

&lt;p&gt;gst is a tool for checking the state of a repository.&lt;/p&gt;

&lt;p&gt;It does not perform operations that modify the repository state, such as &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;merge&lt;/code&gt;, or &lt;code&gt;rebase.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Even if you are not very familiar with Git, there is less risk of accidentally changing something, so I think it is easy to use as a tool for checking repository status.&lt;/p&gt;
&lt;h2&gt;
  
  
  Diff display
&lt;/h2&gt;

&lt;p&gt;In Git, staged changes and unstaged changes in the working tree are managed separately.&lt;/p&gt;

&lt;p&gt;In gst, changed files are displayed with markers such as &lt;code&gt;INDEX&lt;/code&gt;, &lt;code&gt;WORKTREE&lt;/code&gt;, &lt;code&gt;NEW&lt;/code&gt;, and &lt;code&gt;CONFLICT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This makes it easier to understand which files are staged and which files still only have changes in the working tree.&lt;/p&gt;

&lt;p&gt;In the diff view, you can switch between the working tree diff and the staged diff.&lt;/p&gt;

&lt;p&gt;This is useful when you want to check what will be included in the next commit, or when you want to organize your changes before splitting them into separate commits.&lt;/p&gt;
&lt;h2&gt;
  
  
  Copying diffs
&lt;/h2&gt;

&lt;p&gt;In the diff view, you can copy the current diff directly to the clipboard.&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%2F4758g2l888o2k2fs24jk.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%2F4758g2l888o2k2fs24jk.png" alt="yank diff from gst" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;y&lt;/code&gt; to copy working directory diffs, and &lt;code&gt;i&lt;/code&gt; to copy staged diffs.&lt;/p&gt;

&lt;p&gt;Pressing &lt;code&gt;a&lt;/code&gt; copies a patch that includes all changes from &lt;code&gt;HEAD&lt;/code&gt; to the current working directory.&lt;/p&gt;

&lt;p&gt;The copied content is in patch format, so it can be applied with &lt;code&gt;git apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I think this is very useful!&lt;/p&gt;
&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;gst&lt;/strong&gt; is not a tool intended to replace Git operations.&lt;/p&gt;

&lt;p&gt;Rather, it is a tool for checking “what state is this repository in right now?” before you perform an operation.&lt;/p&gt;

&lt;p&gt;Please give it a try :)&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/gst" rel="noopener noreferrer"&gt;
        gst
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Read-only Git status visualizer
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;gst&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;code&gt;gst&lt;/code&gt; is a read-only Git status visualizer for people who want to understand the
shape of a repository before they run Git commands.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/93074851/600953240-09fd03cf-2781-4d9f-b73d-f057e9e1ac90.jpg?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODE3ODYzMDEsIm5iZiI6MTc4MTc4NjAwMSwicGF0aCI6Ii85MzA3NDg1MS82MDA5NTMyNDAtMDlmZDAzY2YtMjc4MS00ZDlmLWI3M2QtZjA1N2U5ZTFhYzkwLmpwZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjE4VDEyMzMyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ0NjY3MzM5ZWE1NDhhMGU0NzhhNzdiZGE2MzYzZDJmMTQ5MjFhN2QwMjk2MzE5ODdkZjM5NTIxN2UwZmU1M2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmpwZWcifQ.DByZhVfdKbOF1Ei03GDBnQkbJw9kfhgxatWGDyJNuJU"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F93074851%2F600953240-09fd03cf-2781-4d9f-b73d-f057e9e1ac90.jpg%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODE3ODYzMDEsIm5iZiI6MTc4MTc4NjAwMSwicGF0aCI6Ii85MzA3NDg1MS82MDA5NTMyNDAtMDlmZDAzY2YtMjc4MS00ZDlmLWI3M2QtZjA1N2U5ZTFhYzkwLmpwZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjE4VDEyMzMyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ0NjY3MzM5ZWE1NDhhMGU0NzhhNzdiZGE2MzYzZDJmMTQ5MjFhN2QwMjk2MzE5ODdkZjM5NTIxN2UwZmU1M2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmpwZWcifQ.DByZhVfdKbOF1Ei03GDBnQkbJw9kfhgxatWGDyJNuJU" alt="Image"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo: &lt;a href="https://youtu.be/EMO3DaNkqT0" rel="nofollow noopener noreferrer"&gt;https://youtu.be/EMO3DaNkqT0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Motivation&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Git beginners often struggle because the current state is split across several
places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;commits and branches form a graph&lt;/li&gt;
&lt;li&gt;local branches and remote tracking branches can point at different commits&lt;/li&gt;
&lt;li&gt;the index and working tree can each contain different file changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;gst&lt;/code&gt; puts those pieces into one terminal dashboard. It does not push, pull
checkout, commit, merge, rebase, or mutate the repository.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go install github.com/lef237/gst/cmd/gst@latest&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Or with Homebrew:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;brew install lef237/tap/gst&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;For local development:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go run ./cmd/gst&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;To build a local binary:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go build -o tmp/gst ./cmd/gst&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Release steps are documented in &lt;a href="https://github.com/lef237/gst/docs/release.md" rel="noopener noreferrer"&gt;docs/release.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;gst
gst --interval 1s
gst --once
gst --no-color&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;By default, &lt;code&gt;gst&lt;/code&gt; opens the interactive TUI.&lt;/p&gt;
&lt;p&gt;Interactive controls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Move between views with &lt;code&gt;tab&lt;/code&gt;, the left/right arrow keys…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/gst" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>git</category>
      <category>go</category>
      <category>cli</category>
      <category>terminal</category>
    </item>
    <item>
      <title>clauhist: browse full Claude Code history and resume sessions across projects</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Sat, 28 Mar 2026 04:43:38 +0000</pubDate>
      <link>https://dev.to/lef237/clauhist-browse-full-claude-code-history-and-resume-sessions-across-projects-1c1o</link>
      <guid>https://dev.to/lef237/clauhist-browse-full-claude-code-history-and-resume-sessions-across-projects-1c1o</guid>
      <description>&lt;p&gt;Claude Code can already help you resume work from a project if you move into that working directory and use &lt;code&gt;/resume&lt;/code&gt; there.&lt;/p&gt;

&lt;p&gt;The limitation is that this is tied to the current working directory. If you want to look back across all of your past work, including sessions from other repositories and directories, that gets awkward.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; is a small CLI tool for that case. It shows your Claude Code history in &lt;code&gt;fzf&lt;/code&gt;, lets you browse sessions across working directories, and resume one from the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it shows
&lt;/h2&gt;

&lt;p&gt;Sessions are sorted by recent activity. Each row includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the last activity time&lt;/li&gt;
&lt;li&gt;the project path&lt;/li&gt;
&lt;li&gt;whether the path still exists&lt;/li&gt;
&lt;li&gt;a preview of the first message&lt;/li&gt;
&lt;li&gt;the message count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a preview pane with the session ID, timestamps, and message list. Once you find the one you want, press &lt;code&gt;Enter&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; reads &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt;, groups entries by session, and passes the result to &lt;code&gt;fzf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After you select a session, it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--resume&lt;/span&gt; &amp;lt;session-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It does not keep its own database or add another history layer. It is just a thin local browser over Claude Code's existing history file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;


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

&lt;/div&gt;


&lt;p&gt;Or install from source:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lef237/clauhist.git
&lt;span class="nb"&gt;cd &lt;/span&gt;clauhist
cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You also need &lt;code&gt;fzf&lt;/code&gt; and Claude Code installed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;


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

&lt;/div&gt;


&lt;p&gt;Main controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Enter&lt;/code&gt;: resume the selected session&lt;/li&gt;
&lt;li&gt;type: filter the list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl-/&lt;/code&gt;: toggle preview&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl-C&lt;/code&gt;: exit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is displayed on the terminal like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────╮
│ Claude Code History Browser  &lt;span class="o"&gt;[&lt;/span&gt;Enter: resume  Ctrl-/: toggle preview  Ctrl-C: cancel]  │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ Search:                                                                               │
│ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 2026-03-18 09:12  ✓ ~/projects/myapp      Tell me about Rust error handling…  &lt;span class="o"&gt;(&lt;/span&gt;12&lt;span class="o"&gt;)&lt;/span&gt;  │
│   2026-03-17 22:45  ✓ ~/sandbox/api-client  Generate client from OpenAPI schema  &lt;span class="o"&gt;(&lt;/span&gt;8&lt;span class="o"&gt;)&lt;/span&gt;  │
│   2026-03-17 14:30  ✗ ~/old-project         Database migration steps             &lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt;  │
╰───────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;✓&lt;/code&gt; means the project directory still exists. &lt;code&gt;✗&lt;/code&gt; means it was moved or deleted. You can still resume the session, but &lt;code&gt;cd&lt;/code&gt; into the original directory may fail.&lt;/p&gt;
&lt;h2&gt;
  
  
  Shell integration
&lt;/h2&gt;

&lt;p&gt;If you add this to your shell config:&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="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;clauhist init zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;then &lt;code&gt;clauhist&lt;/code&gt; will now change to the project directory within your current shell before launching Claude Code.&lt;/p&gt;

&lt;p&gt;This allows you to immediately return to your original directory with &lt;code&gt;cd -&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without this, &lt;code&gt;clauhist&lt;/code&gt; uses a subshell for the directory change, so &lt;code&gt;exit&lt;/code&gt; to return to your original shell.&lt;/p&gt;
&lt;h2&gt;
  
  
  Local-only
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; only reads &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt; and launches &lt;code&gt;claude --resume&lt;/code&gt;. It does not send your history anywhere.&lt;/p&gt;

&lt;p&gt;That is the whole tool: a simple way to browse past Claude Code sessions across projects and reopen one without moving directory by directory.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;You can find the repository here :)&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;
        clauhist
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Browse Claude Code history across working directories and resume sessions
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;clauhist&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Browse Claude Code history across working directories and resume sessions.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────╮
│ Claude Code History Browser  [Enter: resume  Ctrl-/: toggle preview  Ctrl-C: cancel]  │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ Search:                                                                               │
│ &amp;gt; 2026-03-18 09:12  ✓ ~/projects/myapp      Tell me about Rust error handling…  (12)  │
│   2026-03-17 22:45  ✓ ~/sandbox/api-client  Generate client from OpenAPI schema  (8)  │
│   2026-03-17 14:30  ✗ ~/old-project         Database migration steps             (3)  │
╰───────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;(Example output — actual appearance depends on your terminal and fzf version)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Claude Code can already resume work from the current project directory. clauhist solves a different problem: browsing the full history across all of your working directories from one place.&lt;/p&gt;
&lt;p&gt;Select a session and press &lt;code&gt;Enter&lt;/code&gt; — clauhist opens &lt;code&gt;claude --resume&lt;/code&gt; in the project directory. When you exit Claude, you return to your original shell.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why clauhist exists&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;If you &lt;code&gt;cd&lt;/code&gt; into a project and use Claude Code's &lt;code&gt;/resume&lt;/code&gt; there, you can inspect…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;a href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;https://github.com/lef237/clauhist&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>One-Time Editor, an editor for drafts before sending</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Mon, 09 Mar 2026 13:16:35 +0000</pubDate>
      <link>https://dev.to/lef237/one-time-editor-an-editor-for-drafts-before-sending-59gi</link>
      <guid>https://dev.to/lef237/one-time-editor-an-editor-for-drafts-before-sending-59gi</guid>
      <description>&lt;p&gt;I've created an app called "One-Time Editor".&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%2Fidnldxnu6gqyty5xb40f.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%2Fidnldxnu6gqyty5xb40f.png" alt="Dark Mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image above shows dark mode. Here's what it looks like in light mode.&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%2F7069llna9a6mjy7tfcbt.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%2F7069llna9a6mjy7tfcbt.png" alt="Light Mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What kind of app is it, you ask? It's a &lt;strong&gt;text editor that can be used for drafting messages before sending them in a chat&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By using this app, you can avoid the worry of accidentally sending a message prematurely by hitting Enter.&lt;/p&gt;

&lt;p&gt;To quickly understand how it works, I recommend watching the video, which I've attached below.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/qwj9fr77vQg"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="noopener noreferrer"&gt;https://youtu.be/qwj9fr77vQg&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Install
&lt;/h2&gt;

&lt;p&gt;For macOS, use Homebrew. You can start using it right away by typing the following commands in your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap lef237/tap
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; one-time-editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Since it's an Electron-based app, it's also cross-platform compatible.&lt;/p&gt;

&lt;p&gt;Windows and Linux users can install the pre-built app from here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lef237/one-time-editor/releases" rel="noopener noreferrer"&gt;https://github.com/lef237/one-time-editor/releases&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Windows users should select the .exe file, and Linux users should select the AppImage to install it. &lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;p&gt;The main highlight is that you can &lt;strong&gt;toggle (show/hide) the window using a keyboard shortcut&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What's more, &lt;strong&gt;it automatically copies the content you've entered as soon as you toggle it, allowing for instant pasting.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open One-Time Editor with a keyboard shortcut.&lt;/li&gt;
&lt;li&gt;Type the content you want to send.&lt;/li&gt;
&lt;li&gt;Close it with the keyboard shortcut.

&lt;ul&gt;
&lt;li&gt;The content will be automatically copied.&lt;/li&gt;
&lt;li&gt;On macOS, focus will automatically return to the window you were previously using.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Paste and then send.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also supports switching between light and dark modes. I think the design is also stylish.&lt;/p&gt;

&lt;p&gt;Of course, &lt;strong&gt;you can also customize the keyboard shortcut!&lt;/strong&gt; The default is Ctrl+J, but feel free to assign any shortcut you like.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I Made It
&lt;/h2&gt;

&lt;p&gt;Many people have probably experienced indecision about whether to use Enter, Shift+Enter, or Ctrl+Enter to send a chat message.&lt;/p&gt;

&lt;p&gt;And then, accidentally sending a message in the middle of writing...&lt;/p&gt;

&lt;p&gt;Using some kind of editor for drafting isn't bad, but it can be a bit cumbersome to constantly be asked to create a new file or to manage windows.&lt;/p&gt;

&lt;p&gt;This app is designed for throwaway writing, and it allows for displaying/hiding the window and copying content with a single shortcut, making it quite fast.&lt;/p&gt;
&lt;h2&gt;
  
  
  Challenges During Development
&lt;/h2&gt;

&lt;p&gt;There were several challenges.&lt;/p&gt;

&lt;p&gt;However, the most difficult problem was dealing with code signing and notarization.&lt;/p&gt;

&lt;p&gt;When you create an app with Electron, it works fine on your local machine, but if you upload it to the internet and download it, the app becomes corrupted.&lt;/p&gt;

&lt;p&gt;You might have seen this warning when downloading things like Docker:&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%2F44djmq4zivqd2qccfzyq.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%2F44djmq4zivqd2qccfzyq.png" alt="warning"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Therefore, you need to use the &lt;code&gt;xattr&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://osxhub.com/fix-macos-app-damaged-and-cant-be-opened-issue-guide/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.osxhub.com%2F2025%2F06%2Fmacos-app-damaged-and-cant-be-opend-issue" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://osxhub.com/fix-macos-app-damaged-and-cant-be-opened-issue-guide/" rel="noopener noreferrer" class="c-link"&gt;
            Fix macOS app is damaged and can't be opened" Error - Complete Guide - osxhub
          &lt;/a&gt;
        &lt;/h2&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.osxhub.com%2Fuploads%2F2025%2F06%2Ffavicon.png"&gt;
          osxhub.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;This means you need to run the command &lt;code&gt;xattr -rc "/Applications/hoge.app"&lt;/code&gt; after installation, which is a bit troublesome.&lt;/p&gt;

&lt;p&gt;When I looked into simplifying this, I realized that using &lt;code&gt;postflight&lt;/code&gt; could automate this process if installed via homebrew-tap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lef237/homebrew-tap/blob/main/Casks/one-time-editor.rb" rel="noopener noreferrer"&gt;https://github.com/lef237/homebrew-tap/blob/main/Casks/one-time-editor.rb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're looking to distribute Electron apps, please use this as a reference!&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;GitHub is also public.&lt;/p&gt;

&lt;p&gt;If you have any feature requests, please open an Issue. 🐘&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;
        one-time-editor
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A lightweight scratchpad that lives one shortcut away
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;One-Time Editor&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A lightweight scratchpad that lives one shortcut away. Draft a message, hit the shortcut again, and it's already on your clipboard — ready to paste anywhere.&lt;/p&gt;
&lt;p&gt;Built for the workflow of writing chat messages, AI prompts, and quick notes that you type once and send.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/831acaaa14e6e538238c6c9af7341628428f9638d6ebab57e7052245eaf3937b/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f71776a39667237377651672f6d617872657364656661756c742e6a7067" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="nofollow noopener noreferrer"&gt;https://youtu.be/qwj9fr77vQg&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;macOS (Homebrew)&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;brew tap lef237/tap
brew install --cask one-time-editor&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Manual download&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Pre-built binaries for macOS, Windows, and Linux are available on the &lt;a href="https://github.com/lef237/one-time-editor/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; page.&lt;/p&gt;
&lt;p&gt;If you download manually on macOS, the app is not signed with an Apple Developer certificate, so macOS may show a warning. To allow it, run:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;xattr -cr &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;/Applications/One-Time Editor.app&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl+J&lt;/code&gt; to summon the editor&lt;/li&gt;
&lt;li&gt;Type your text&lt;/li&gt;
&lt;li&gt;Press the shortcut again — the window disappears and your text is copied to clipboard&lt;/li&gt;
&lt;li&gt;Paste wherever you need it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That's it. No save dialog, no file management, no friction.&lt;/p&gt;
&lt;p&gt;The shortcut is…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;&lt;a href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;https://github.com/lef237/one-time-editor&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;If it doesn't work on Windows or other OSs, please comment on GitHub!&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>productivity</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Writing a Domain Model in Ruby Without Using class</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Wed, 13 Aug 2025 10:47:28 +0000</pubDate>
      <link>https://dev.to/lef237/writing-a-domain-model-in-ruby-without-using-class-5h06</link>
      <guid>https://dev.to/lef237/writing-a-domain-model-in-ruby-without-using-class-5h06</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;From Ruby 3.2, the &lt;code&gt;Data&lt;/code&gt; class has been introduced, allowing us to explore ways of representing domain models without using &lt;code&gt;class&lt;/code&gt;. In this article, I’ll examine how we can do that.&lt;/p&gt;

&lt;p&gt;When writing business logic in Ruby, we naturally tend to use &lt;code&gt;class&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We set attributes in &lt;code&gt;initialize&lt;/code&gt;, and change state via instance methods. Especially if you’ve been writing Rails code, this feels natural.&lt;/p&gt;

&lt;p&gt;However, whether managing logic with a stateful class is suitable for &lt;em&gt;every&lt;/em&gt; case is debatable. For domain logic where we want to avoid side effects, class-based design might not always be optimal. In recent years, designs influenced by functional programming have gained attention, and as has been discussed on X (formerly Twitter), the mainstream approach in TypeScript is moving toward avoiding classes entirely.&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;Data&lt;/code&gt; class introduced in Ruby 3.2, we can represent domains while keeping structural data and logic separate. In this article, I’ll show you how.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This article focuses on &lt;em&gt;representing domain logic&lt;/em&gt;, separate from Rails' ActiveRecord models.&lt;br&gt;&lt;br&gt;
I’m specifically looking at a style that works purely with plain Ruby syntax, without being tied to the Rails environment. This is driven more by curiosity than by real-world constraints, so please keep that in mind.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is the &lt;code&gt;Data&lt;/code&gt; Class?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Data&lt;/code&gt; class introduced in Ruby 3.2 lets you define immutable data structures easily. It’s similar to the &lt;a href="https://docs.ruby-lang.org/en/master/Struct.html" rel="noopener noreferrer"&gt;Struct&lt;/a&gt; that Ruby already had, but all fields are immutable — to change a value, you must create a new instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.ruby-lang.org/en/master/Data.html" rel="noopener noreferrer"&gt;class Data - Documentation for Ruby&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To use a loose analogy: Struct is like a hash, while Data is like a read-only hash.&lt;/p&gt;

&lt;p&gt;Let’s actually design a domain model using the &lt;code&gt;Data&lt;/code&gt; class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Domain Model
&lt;/h2&gt;

&lt;p&gt;First, let’s think of an example.&lt;/p&gt;

&lt;p&gt;We’ll imagine a &lt;code&gt;Customer&lt;/code&gt; model with the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;first_name&lt;/code&gt; / &lt;code&gt;last_name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_active&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; / &lt;code&gt;updated_at&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We want to be able to perform operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing the name&lt;/li&gt;
&lt;li&gt;Changing the email address&lt;/li&gt;
&lt;li&gt;Activating/deactivating the customer&lt;/li&gt;
&lt;li&gt;Getting the full name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially the first three operations would naturally become methods that change state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Class-Based Implementation
&lt;/h2&gt;

&lt;p&gt;First, let’s design it in the usual class-based way. You might find there are too many instance variables, among other nitpicks, but let’s just write it out as a sample.&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;Customer&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:id&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;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;
    &lt;span class="vi"&gt;@email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="vi"&gt;@first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;
    &lt;span class="vi"&gt;@last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="vi"&gt;@updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;
    &lt;span class="vi"&gt;@last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
    &lt;span class="n"&gt;touch&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;change_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="n"&gt;touch&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;deactivate&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;touch&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;activate&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&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;touch&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;full_name&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

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

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;touch&lt;/span&gt;
    &lt;span class="vi"&gt;@updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&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 name"&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;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A[^@\s]+@[^@\s]+\z/&lt;/span&gt;&lt;span class="p"&gt;)&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 email format"&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;This can be used as follows:&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;customer&lt;/span&gt; &lt;span class="o"&gt;=&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;new&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;"c1"&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;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Anderson"&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="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Brown"&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="nf"&gt;deactivate&lt;/span&gt;
&lt;span class="nb"&gt;puts&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;full_name&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Bob Brown"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we create an instance with &lt;code&gt;new&lt;/code&gt; and call methods to change its state — a very common approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;Data&lt;/code&gt; Class Available from Ruby 3.2
&lt;/h2&gt;

&lt;p&gt;In Ruby 3.2, the &lt;code&gt;Data&lt;/code&gt; class was introduced.&lt;/p&gt;

&lt;p&gt;It feels similar to &lt;code&gt;Struct&lt;/code&gt;, but all fields are immutable.&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;Customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&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="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change a value, you use &lt;code&gt;.with&lt;/code&gt; to create a new instance.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;customer1&lt;/code&gt; has &lt;code&gt;first_name&lt;/code&gt; set to &lt;code&gt;Bob&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer1&lt;/span&gt; &lt;span class="o"&gt;=&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;new&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;"c1"&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;"bob@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Johnson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;is_active: &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;created_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change &lt;code&gt;first_name&lt;/code&gt; to &lt;code&gt;Carol&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Carol"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;customer1&lt;/code&gt; remains unchanged; &lt;code&gt;customer2&lt;/code&gt; is a new object with only &lt;code&gt;first_name&lt;/code&gt; different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; "Bob"&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; "Carol"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;Data.with&lt;/code&gt;, we handle state not by mutation but by replacement, avoiding unintended side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Domain Logic with &lt;code&gt;Data&lt;/code&gt; and Functions
&lt;/h2&gt;

&lt;p&gt;Let’s rewrite the earlier class-based code using &lt;code&gt;Data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We’ll define a &lt;code&gt;CustomerService&lt;/code&gt; module, separate from &lt;code&gt;Customer&lt;/code&gt;, to hold our domain operations as functions.&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;Customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&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="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CustomerService&lt;/span&gt;
  &lt;span class="kp"&gt;module_function&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;now&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;change_name&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;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&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="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;change_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;email&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;deactivate&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;unless&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;is_active&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;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;activate&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;if&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;is_active&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;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;is_active: &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;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;full_name&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="s2"&gt;"&lt;/span&gt;&lt;span class="si"&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;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&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 name"&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;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A[^@\s]+@[^@\s]+\z/&lt;/span&gt;&lt;span class="p"&gt;)&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 email format"&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;Here, &lt;code&gt;create_customer&lt;/code&gt; is used as a factory in &lt;code&gt;CustomerService&lt;/code&gt;. By defining domain operations as functions, we can handle changes to state in a way that minimizes side effects.&lt;/p&gt;

&lt;p&gt;Each function doesn’t change the &lt;code&gt;Customer&lt;/code&gt; data structure directly — it uses &lt;code&gt;.with&lt;/code&gt; to create a new instance. This preserves data immutability and brings the design closer to a functional style.&lt;/p&gt;

&lt;p&gt;Also, by using &lt;code&gt;module_function&lt;/code&gt;, we can call these functions without creating an instance of the module. Validation is performed inside the functions where needed.&lt;/p&gt;

&lt;p&gt;This cleanly separates data (&lt;code&gt;Customer&lt;/code&gt;) from behavior (functions).&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Usage
&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;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_customer&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;"c1"&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;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Anderson"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_name&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="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Brown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deactivate&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="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_name&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="c1"&gt;# =&amp;gt; "Bob Brown"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that when reassigning, we return a &lt;em&gt;new&lt;/em&gt; object instead of modifying the original. I considered naming them &lt;code&gt;customer_updated&lt;/code&gt;, &lt;code&gt;customer_updated2&lt;/code&gt;, etc., but left it as-is for now.&lt;/p&gt;

&lt;p&gt;By not using instance variables and passing all state through arguments, we avoid side effects. At that point, all that’s left is to store the final state in the database or handle it however needed.&lt;/p&gt;

&lt;p&gt;For example, saving with SQLite might look 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="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"INSERT OR REPLACE INTO customers (id, email, first_name, last_name, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"&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="nf"&gt;id&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="nf"&gt;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="nf"&gt;first_name&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="nf"&gt;last_name&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="nf"&gt;is_active&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&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="nf"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can access attributes with &lt;code&gt;customer.xxx&lt;/code&gt;, so it feels similar to using a class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points to Note
&lt;/h2&gt;

&lt;p&gt;Of course, there’s nothing wrong with using &lt;code&gt;class&lt;/code&gt;. For UI logic, controllers, database models like ActiveRecord, and so on, class-based design often makes sense.&lt;/p&gt;

&lt;p&gt;In Rails projects, it’s common to extract structured data handling into POROs (Plain Old Ruby Objects) written as classes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/sulmanweb/plain-old-ruby-objects-poros-in-rails-fat-models-3l7f"&gt;Plain Old Ruby Objects (POROs) in Rails Fat Models - DEV Community&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, as domain logic grows more complex, tracking “state changes” through the code becomes harder. In such cases, a design where “values are immutable and logic is functional” — like the one shown here — can make maintenance easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Ruby is a flexible language, and you can represent domain logic without relying on classes or instance variables.&lt;/p&gt;

&lt;p&gt;Ruby 3.2’s &lt;code&gt;Data&lt;/code&gt; class can be a useful tool for designing the domain layer as structured data plus explicit functions. You don’t have to use it for every case, but simply knowing it’s an option can broaden your design possibilities.&lt;/p&gt;

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